)]}'
{"version": 3, "sources": ["/web/static/lib/luxon/luxon.js", "/web/static/src/polyfills/object.js", "/web/static/src/polyfills/array.js", "/web_editor/static/src/js/frontend/loader_loading.js", "/web/static/lib/owl/owl.js", "/web/static/lib/owl/odoo_module.js", "/web/static/lib/jquery/jquery.js", "/web/static/lib/popper/popper.js", "/web/static/lib/bootstrap/js/dist/util/index.js", "/web/static/lib/bootstrap/js/dist/dom/data.js", "/web/static/lib/bootstrap/js/dist/dom/event-handler.js", "/web/static/lib/bootstrap/js/dist/dom/manipulator.js", "/web/static/lib/bootstrap/js/dist/dom/selector-engine.js", "/web/static/lib/bootstrap/js/dist/util/config.js", "/web/static/lib/bootstrap/js/dist/util/component-functions.js", "/web/static/lib/bootstrap/js/dist/util/backdrop.js", "/web/static/lib/bootstrap/js/dist/util/focustrap.js", "/web/static/lib/bootstrap/js/dist/util/sanitizer.js", "/web/static/lib/bootstrap/js/dist/util/scrollbar.js", "/web/static/lib/bootstrap/js/dist/util/swipe.js", "/web/static/lib/bootstrap/js/dist/util/template-factory.js", "/web/static/lib/bootstrap/js/dist/base-component.js", "/web/static/lib/bootstrap/js/dist/alert.js", "/web/static/lib/bootstrap/js/dist/button.js", "/web/static/lib/bootstrap/js/dist/carousel.js", "/web/static/lib/bootstrap/js/dist/collapse.js", "/web/static/lib/bootstrap/js/dist/dropdown.js", "/web/static/lib/bootstrap/js/dist/modal.js", "/web/static/lib/bootstrap/js/dist/offcanvas.js", "/web/static/lib/bootstrap/js/dist/tooltip.js", "/web/static/lib/bootstrap/js/dist/popover.js", "/web/static/lib/bootstrap/js/dist/scrollspy.js", "/web/static/lib/bootstrap/js/dist/tab.js", "/web/static/lib/bootstrap/js/dist/toast.js", "/web/static/src/libs/bootstrap.js", "/web/static/src/legacy/js/libs/jquery.js", "/web/static/src/legacy/js/core/class.js", "/web/static/src/env.js", "/web/static/src/core/action_swiper/action_swiper.js", "/web/static/src/core/anchor_scroll_prevention.js", "/web/static/src/core/assets.js", "/web/static/src/core/autocomplete/autocomplete.js", "/web/static/src/core/barcode/ZXingBarcodeDetector.js", "/web/static/src/core/barcode/barcode_dialog.js", "/web/static/src/core/barcode/barcode_video_scanner.js", "/web/static/src/core/barcode/crop_overlay.js", "/web/static/src/core/browser/browser.js", "/web/static/src/core/browser/feature_detection.js", "/web/static/src/core/browser/router.js", "/web/static/src/core/checkbox/checkbox.js", "/web/static/src/core/code_editor/code_editor.js", "/web/static/src/core/colorlist/colorlist.js", "/web/static/src/core/colorpicker/colorpicker.js", "/web/static/src/core/colors/colors.js", "/web/static/src/core/confirmation_dialog/confirmation_dialog.js", "/web/static/src/core/context.js", "/web/static/src/core/copy_button/copy_button.js", "/web/static/src/core/currency.js", "/web/static/src/core/datetime/datetime_hook.js", "/web/static/src/core/datetime/datetime_input.js", "/web/static/src/core/datetime/datetime_picker.js", "/web/static/src/core/datetime/datetime_picker_popover.js", "/web/static/src/core/datetime/datetimepicker_service.js", "/web/static/src/core/debug/debug_context.js", "/web/static/src/core/debug/debug_menu_basic.js", "/web/static/src/core/debug/debug_menu_items.js", "/web/static/src/core/debug/debug_providers.js", "/web/static/src/core/debug/debug_utils.js", "/web/static/src/core/dialog/dialog.js", "/web/static/src/core/dialog/dialog_service.js", "/web/static/src/core/domain.js", "/web/static/src/core/domain_selector/domain_selector.js", "/web/static/src/core/domain_selector/domain_selector_operator_editor.js", "/web/static/src/core/domain_selector/utils.js", "/web/static/src/core/domain_selector_dialog/domain_selector_dialog.js", "/web/static/src/core/dropdown/_behaviours/dropdown_group_hook.js", "/web/static/src/core/dropdown/_behaviours/dropdown_nesting.js", "/web/static/src/core/dropdown/_behaviours/dropdown_popover.js", "/web/static/src/core/dropdown/accordion_item.js", "/web/static/src/core/dropdown/checkbox_item.js", "/web/static/src/core/dropdown/dropdown.js", "/web/static/src/core/dropdown/dropdown_group.js", "/web/static/src/core/dropdown/dropdown_hooks.js", "/web/static/src/core/dropdown/dropdown_item.js", "/web/static/src/core/dropzone/dropzone.js", "/web/static/src/core/dropzone/dropzone_hook.js", "/web/static/src/core/effects/effect_service.js", "/web/static/src/core/effects/rainbow_man.js", "/web/static/src/core/emoji_picker/emoji_picker.js", "/web/static/src/core/ensure_jquery.js", "/web/static/src/core/errors/error_dialogs.js", "/web/static/src/core/errors/error_handlers.js", "/web/static/src/core/errors/error_service.js", "/web/static/src/core/errors/error_utils.js", "/web/static/src/core/errors/scss_error_dialog.js", "/web/static/src/core/expression_editor/expression_editor.js", "/web/static/src/core/expression_editor/expression_editor_operator_editor.js", "/web/static/src/core/expression_editor_dialog/expression_editor_dialog.js", "/web/static/src/core/field_service.js", "/web/static/src/core/file_input/file_input.js", "/web/static/src/core/file_upload/file_upload_progress_bar.js", "/web/static/src/core/file_upload/file_upload_progress_container.js", "/web/static/src/core/file_upload/file_upload_progress_record.js", "/web/static/src/core/file_upload/file_upload_service.js", "/web/static/src/core/file_viewer/file_model.js", "/web/static/src/core/file_viewer/file_viewer.js", "/web/static/src/core/file_viewer/file_viewer_hook.js", "/web/static/src/core/hotkeys/hotkey_hook.js", "/web/static/src/core/hotkeys/hotkey_service.js", "/web/static/src/core/install_scoped_app/install_scoped_app.js", "/web/static/src/core/l10n/dates.js", "/web/static/src/core/l10n/localization.js", "/web/static/src/core/l10n/localization_service.js", "/web/static/src/core/l10n/translation.js", "/web/static/src/core/l10n/utils.js", "/web/static/src/core/l10n/utils/format_list.js", "/web/static/src/core/l10n/utils/locales.js", "/web/static/src/core/macro.js", "/web/static/src/core/main_components_container.js", "/web/static/src/core/model_field_selector/model_field_selector.js", "/web/static/src/core/model_field_selector/model_field_selector_popover.js", "/web/static/src/core/model_field_selector/utils.js", "/web/static/src/core/model_selector/model_selector.js", "/web/static/src/core/name_service.js", "/web/static/src/core/navigation/navigation.js", "/web/static/src/core/network/download.js", "/web/static/src/core/network/http_service.js", "/web/static/src/core/network/rpc.js", "/web/static/src/core/notebook/notebook.js", "/web/static/src/core/notifications/notification.js", "/web/static/src/core/notifications/notification_container.js", "/web/static/src/core/notifications/notification_service.js", "/web/static/src/core/orm_service.js", "/web/static/src/core/overlay/overlay_container.js", "/web/static/src/core/overlay/overlay_service.js", "/web/static/src/core/pager/pager.js", "/web/static/src/core/pager/pager_indicator.js", "/web/static/src/core/popover/popover.js", "/web/static/src/core/popover/popover_hook.js", "/web/static/src/core/popover/popover_service.js", "/web/static/src/core/position/position_hook.js", "/web/static/src/core/position/utils.js", "/web/static/src/core/pwa/install_prompt.js", "/web/static/src/core/pwa/pwa_service.js", "/web/static/src/core/py_js/py.js", "/web/static/src/core/py_js/py_builtin.js", "/web/static/src/core/py_js/py_date.js", "/web/static/src/core/py_js/py_interpreter.js", "/web/static/src/core/py_js/py_parser.js", "/web/static/src/core/py_js/py_tokenizer.js", "/web/static/src/core/py_js/py_utils.js", "/web/static/src/core/record_selectors/multi_record_selector.js", "/web/static/src/core/record_selectors/record_autocomplete.js", "/web/static/src/core/record_selectors/record_selector.js", "/web/static/src/core/record_selectors/tag_navigation_hook.js", "/web/static/src/core/registry.js", "/web/static/src/core/registry_hook.js", "/web/static/src/core/resizable_panel/resizable_panel.js", "/web/static/src/core/select_menu/select_menu.js", "/web/static/src/core/signature/name_and_signature.js", "/web/static/src/core/signature/signature_dialog.js", "/web/static/src/core/tags_list/tags_list.js", "/web/static/src/core/template_inheritance.js", "/web/static/src/core/templates.js", "/web/static/src/core/tooltip/tooltip.js", "/web/static/src/core/tooltip/tooltip_hook.js", "/web/static/src/core/tooltip/tooltip_service.js", "/web/static/src/core/transition.js", "/web/static/src/core/tree_editor/condition_tree.js", "/web/static/src/core/tree_editor/tree_editor.js", "/web/static/src/core/tree_editor/tree_editor_autocomplete.js", "/web/static/src/core/tree_editor/tree_editor_components.js", "/web/static/src/core/tree_editor/tree_editor_operator_editor.js", "/web/static/src/core/tree_editor/tree_editor_value_editors.js", "/web/static/src/core/tree_editor/utils.js", "/web/static/src/core/ui/block_ui.js", "/web/static/src/core/ui/ui_service.js", "/web/static/src/core/user.js", "/web/static/src/core/user_switch/user_switch.js", "/web/static/src/core/utils/arrays.js", "/web/static/src/core/utils/autoresize.js", "/web/static/src/core/utils/binary.js", "/web/static/src/core/utils/cache.js", "/web/static/src/core/utils/classname.js", "/web/static/src/core/utils/colors.js", "/web/static/src/core/utils/components.js", "/web/static/src/core/utils/concurrency.js", "/web/static/src/core/utils/draggable.js", "/web/static/src/core/utils/draggable_hook_builder.js", "/web/static/src/core/utils/draggable_hook_builder_owl.js", "/web/static/src/core/utils/files.js", "/web/static/src/core/utils/functions.js", "/web/static/src/core/utils/hooks.js", "/web/static/src/core/utils/misc.js", "/web/static/src/core/utils/nested_sortable.js", "/web/static/src/core/utils/numbers.js", "/web/static/src/core/utils/objects.js", "/web/static/src/core/utils/patch.js", "/web/static/src/core/utils/reactive.js", "/web/static/src/core/utils/render.js", "/web/static/src/core/utils/scrolling.js", "/web/static/src/core/utils/search.js", "/web/static/src/core/utils/sortable.js", "/web/static/src/core/utils/sortable_owl.js", "/web/static/src/core/utils/sortable_service.js", "/web/static/src/core/utils/strings.js", "/web/static/src/core/utils/timing.js", "/web/static/src/core/utils/urls.js", "/web/static/src/core/utils/xml.js", "/web/static/src/core/virtual_grid_hook.js", "/web/static/src/core/commands/default_providers.js", "/web/static/src/core/commands/command_palette.js", "/web/static/src/public/error_notifications.js", "/web/static/src/public/public_component_service.js", "/web/static/src/public/datetime_picker_widget.js", "/web/static/src/libs/pdfjs.js", "/web/static/src/legacy/js/public/public_root.js", "/website/static/src/js/content/website_root_instance.js", "/web/static/src/legacy/js/public/public_widget.js", "/web/static/src/legacy/js/public/signin.js", "/bus/static/src/bus_parameters_service.js", "/bus/static/src/im_status_service.js", "/bus/static/src/misc.js", "/bus/static/src/multi_tab_service.js", "/bus/static/src/services/bus_service.js", "/bus/static/src/services/presence_service.js", "/bus/static/src/workers/websocket_worker.js", "/bus/static/src/workers/websocket_worker_utils.js", "/web_tour/static/src/tour_pointer/tour_pointer.js", "/web_tour/static/src/tour_service/tour_automatic.js", "/web_tour/static/src/tour_service/tour_helpers.js", "/web_tour/static/src/tour_service/tour_interactive.js", "/web_tour/static/src/tour_service/tour_pointer_state.js", "/web_tour/static/src/tour_service/tour_recorder/tour_recorder.js", "/web_tour/static/src/tour_service/tour_service.js", "/web_tour/static/src/tour_service/tour_state.js", "/web_tour/static/src/tour_service/tour_step.js", "/web_tour/static/src/tour_service/tour_step_automatic.js", "/web_tour/static/src/tour_service/tour_utils.js", "/web/static/lib/hoot-dom/helpers/dom.js", "/web/static/lib/hoot-dom/helpers/events.js", "/web/static/lib/hoot-dom/helpers/time.js", "/web/static/lib/hoot-dom/hoot-dom.js", "/web/static/lib/hoot-dom/hoot_dom_utils.js", "/html_editor/static/src/main/media/media_dialog/document_selector.js", "/html_editor/static/src/main/media/media_dialog/file_documents_selector.js", "/html_editor/static/src/main/media/media_dialog/file_media_dialog.js", "/html_editor/static/src/main/media/media_dialog/file_selector.js", "/html_editor/static/src/main/media/media_dialog/icon_selector.js", "/html_editor/static/src/main/media/media_dialog/image_selector.js", "/html_editor/static/src/main/media/media_dialog/media_dialog.js", "/html_editor/static/src/main/media/media_dialog/search_media.js", "/html_editor/static/src/main/media/media_dialog/upload_progress_toast/upload_progress_toast.js", "/html_editor/static/src/main/media/media_dialog/upload_progress_toast/upload_service.js", "/html_editor/static/src/main/media/media_dialog/video_selector.js", "/html_editor/static/src/utils/blocks.js", "/html_editor/static/src/utils/color.js", "/html_editor/static/src/utils/content_types.js", "/html_editor/static/src/utils/dom.js", "/html_editor/static/src/utils/dom_info.js", "/html_editor/static/src/utils/dom_state.js", "/html_editor/static/src/utils/dom_traversal.js", "/html_editor/static/src/utils/drag_and_drop.js", "/html_editor/static/src/utils/fonts.js", "/html_editor/static/src/utils/formatting.js", "/html_editor/static/src/utils/html.js", "/html_editor/static/src/utils/image.js", "/html_editor/static/src/utils/image_processing.js", "/html_editor/static/src/utils/list.js", "/html_editor/static/src/utils/perspective_utils.js", "/html_editor/static/src/utils/position.js", "/html_editor/static/src/utils/regex.js", "/html_editor/static/src/utils/resource.js", "/html_editor/static/src/utils/sanitize.js", "/html_editor/static/src/utils/selection.js", "/html_editor/static/src/utils/table.js", "/html_editor/static/src/utils/url.js", "/web_unsplash/static/src/media_dialog/image_selector_patch.js", "/web_unsplash/static/src/media_dialog/media_dialog_patch.js", "/web_unsplash/static/src/unsplash_credentials/unsplash_credentials.js", "/web_unsplash/static/src/unsplash_error/unsplash_error.js", "/web_unsplash/static/src/unsplash_service.js", "/web_editor/static/src/components/media_dialog/document_selector.js", "/web_editor/static/src/components/media_dialog/file_selector.js", "/web_editor/static/src/components/media_dialog/icon_selector.js", "/web_editor/static/src/components/media_dialog/image_selector.js", "/web_editor/static/src/components/media_dialog/media_dialog.js", "/web_editor/static/src/components/media_dialog/search_media.js", "/web_editor/static/src/components/media_dialog/video_selector.js", "/web_editor/static/src/components/upload_progress_toast/upload_progress_toast.js", "/web_editor/static/src/components/upload_progress_toast/upload_service.js", "/website/static/src/components/media_dialog/file_documents_selector.js", "/website/static/src/components/media_dialog/image_selector.js", "/web_unsplash/static/src/media_dialog_legacy/image_selector.js", "/web_editor/static/src/js/common/browser_extensions.js", "/web_editor/static/src/js/common/column_layout_mixin.js", "/web_editor/static/src/js/common/grid_layout_utils.js", "/web_editor/static/src/js/common/scrolling.js", "/web_editor/static/src/js/common/utils.js", "/web_editor/static/src/js/common/wysiwyg_utils.js", "/web_editor/static/src/js/core/owl_utils.js", "/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js", "/web_editor/static/src/js/wysiwyg/fonts.js", "/web_editor/static/src/js/frontend/loadWysiwygFromTextarea.js", "/auth_signup/static/src/js/signup.js", "/portal/static/src/js/portal.js", "/portal/static/src/js/portal_composer.js", "/portal/static/src/js/portal_security.js", "/portal/static/src/js/portal_sidebar.js", "/portal/static/src/js/components/input_confirmation_dialog/input_confirmation_dialog.js", "/portal/static/src/signature_form/signature_form.js", "/portal/static/src/chatter/boot/boot_service.js", "/account/static/src/js/account_portal_sidebar.js", "/account/static/src/js/account_portal.js", "/account/static/src/components/tests_shared_js_python/tests_shared_js_python.js", "/account/static/src/helpers/account_tax.js", "/account/static/src/core/utils/product_and_label_autoresize.js", "/payment/static/lib/jquery.payment/jquery.payment.js", "/payment/static/src/js/express_checkout_form.js", "/payment/static/src/js/payment_button.js", "/payment/static/src/js/payment_form.js", "/payment/static/src/js/post_processing.js", "/account_payment/static/src/js/payment_form.js", "/account_payment/static/src/js/portal_invoice_page_payment.js", "/account_payment/static/src/js/portal_my_invoices_payment.js", "/sale/static/src/js/sale_portal_sidebar.js", "/sale/static/src/js/sale_portal_prepayment.js", "/sale/static/src/js/sale_portal.js", "/sale_management/static/src/js/sale_management.js", "/google_recaptcha/static/src/js/recaptcha.js", "/website/static/src/libs/zoomodoo/zoomodoo.js", "/website/static/src/libs/bootstrap/bootstrap.js", "/website/static/src/js/utils.js", "/website/static/src/components/autocomplete_with_pages/autocomplete_with_pages.js", "/website/static/src/components/autocomplete_with_pages/url_autocomplete.js", "/website/static/src/js/tours/tour_utils.js", "/website/static/src/js/content/website_root.js", "/website/static/src/js/content/compatibility.js", "/website/static/src/js/content/menu.js", "/website/static/src/js/content/snippets.animation.js", "/website/static/src/js/show_password.js", "/website/static/src/js/post_link.js", "/website/static/src/js/plausible.js", "/website/static/src/js/website_controller_page_listing_layout.js", "/website/static/src/js/user_custom_javascript.js", "/website/static/src/js/http_cookie.js", "/website/static/src/js/text_processing.js", "/website/static/src/snippets/observing_cookie_mixin.js", "/purchase/static/src/js/purchase_datetimepicker.js", "/purchase/static/src/js/purchase_portal_sidebar.js", "/project/static/src/js/portal_rating.js", "/website_payment/static/src/js/payment_form.js", "/website_payment/static/src/js/website_payment_donation.js", "/website_mail/static/src/js/follow.js", "/portal_rating/static/src/js/portal_composer.js", "/portal_rating/static/src/js/portal_rating_composer.js", "/website_sale/static/src/js/tours/tour_utils.js", "/website_sale/static/src/js/address.js", "/website_sale/static/src/js/cart.js", "/website_sale/static/src/js/checkout.js", "/website_sale/static/src/js/payment_button.js", "/website_sale/static/src/js/payment_form.js", "/website_sale/static/src/js/sale_variant_mixin.js", "/website_sale/static/src/js/terms_and_conditions_checkbox.js", "/website_sale/static/src/js/website_sale.js", "/website_sale/static/src/js/website_sale_offcanvas.js", "/website_sale/static/src/js/website_sale_price_range_option.js", "/website_sale/static/src/js/website_sale_configurators.js", "/website_sale/static/src/js/website_sale_utils.js", "/website_sale/static/src/js/website_sale_recently_viewed.js", "/website_sale/static/src/js/website_sale_tracking.js", "/website/static/lib/multirange/multirange_custom.js", "/website/static/lib/multirange/multirange_instance.js", "/website_sale/static/src/js/components/website_sale_image_viewer.js", "/website_sale/static/src/js/website_sale_reorder.js", "/website_sale/static/src/js/notification/add_to_cart_notification/add_to_cart_notification.js", "/website_sale/static/src/js/notification/cart_notification/cart_notification.js", "/website_sale/static/src/js/notification/warning_notification/warning_notification.js", "/website_sale/static/src/js/notification/notification_service.js", "/sale/static/src/js/badge_extra_price/badge_extra_price.js", "/sale/static/src/js/combo_configurator_dialog/combo_configurator_dialog.js", "/sale/static/src/js/models/product_combo.js", "/sale/static/src/js/models/product_combo_item.js", "/sale/static/src/js/models/product_product.js", "/sale/static/src/js/models/product_template_attribute_line.js", "/sale/static/src/js/models/product_template_attribute_value.js", "/sale/static/src/js/product/product.js", "/sale/static/src/js/product_card/product_card.js", "/sale/static/src/js/product_configurator_dialog/product_configurator_dialog.js", "/sale/static/src/js/product_list/product_list.js", "/sale/static/src/js/product_template_attribute_line/product_template_attribute_line.js", "/sale/static/src/js/quantity_buttons/quantity_buttons.js", "/sale/static/src/js/sale_utils.js", "/website_sale/static/src/js/combo_configurator_dialog/combo_configurator_dialog.js", "/website_sale/static/src/js/product/product.js", "/website_sale/static/src/js/product_configurator_dialog/product_configurator_dialog.js", "/website_sale/static/src/js/product_list/product_list.js", "/website_sale/static/src/js/product_template_attribute_line/product_template_attribute_line.js", "/delivery/static/src/js/location_selector/location/location.js", "/delivery/static/src/js/location_selector/location_list/location_list.js", "/delivery/static/src/js/location_selector/location_schedule/location_schedule.js", "/delivery/static/src/js/location_selector/location_selector_dialog/location_selector_dialog.js", "/delivery/static/src/js/location_selector/map/map.js", "/delivery/static/src/js/location_selector/map_container/map_container.js", "/website_sale/static/src/js/location_selector/location_selector_dialog/location_selector_dialog.js", "/mass_mailing/static/src/js/tours/mass_mailing_tour.js", "/web_unsplash/static/src/frontend/unsplash_beacon.js", "/knowledge/static/src/js/knowledge_utils.js", "/sign/static/src/components/sign_request/PDF_iframe.js", "/sign/static/src/components/sign_request/document_signable.js", "/sign/static/src/components/sign_request/mobile_input_bottom_sheet.js", "/sign/static/src/components/sign_request/sign_item_navigator.js", "/sign/static/src/components/sign_request/signable_PDF_iframe.js", "/sign/static/src/components/sign_request/utils.js", "/sign/static/src/dialogs/dialogs.js", "/sign/static/src/dialogs/encrypted_dialog.js", "/sign/static/src/dialogs/initials_all_pages_dialog.js", "/sign/static/src/dialogs/next_direct_sign_dialog.js", "/sign/static/src/dialogs/public_signer_dialog.js", "/sign/static/src/dialogs/sign_name_and_signature_dialog.js", "/sign/static/src/dialogs/sign_refusal_dialog.js", "/sign/static/src/dialogs/sms_signer_dialog.js", "/sign/static/src/dialogs/thank_you_dialog.js", "/sign/static/src/services/sign_info_service.js", "/website_profile/static/src/js/website_profile.js", "/website_slides/static/src/js/slides.js", "/website_slides/static/src/js/slides_share.js", "/website_slides/static/src/js/slides_upload.js", "/website_slides/static/src/js/slides_category_add.js", "/website_slides/static/src/js/slides_category_delete.js", "/website_slides/static/src/js/slides_slide_archive.js", "/website_slides/static/src/js/slides_slide_toggle_is_preview.js", "/website_slides/static/src/js/slides_slide_like.js", "/website_slides/static/src/js/slides_course_page.js", "/website_slides/static/src/js/slides_course_slides_list.js", "/website_slides/static/src/js/slides_course_fullscreen_player.js", "/website_slides/static/src/js/slides_course_join.js", "/website_slides/static/src/js/slides_course_enroll_email.js", "/website_slides/static/src/js/slides_course_prerequisite.js", "/website_slides/static/src/js/slides_course_quiz.js", "/website_slides/static/src/js/slides_course_quiz_question_form.js", "/website_slides/static/src/js/slides_course_tag_add.js", "/website_slides/static/src/js/slides_course_unsubscribe.js", "/website_slides/static/src/js/portal_rating_composer.js", "/website_slides/static/src/js/public/components/category_add_dialog/category_add_dialog.js", "/website_slides/static/src/js/public/components/course_tag_add_dialog/course_tag_add_dialog.js", "/website_slides/static/src/js/public/components/slide_quiz_finish_dialog/slide_quiz_finish_dialog.js", "/website_slides/static/src/js/public/components/slide_quiz_finish_dialog/slide_xp_progress_bar.js", "/website_slides/static/src/js/public/components/slide_share_dialog/email_sharing_input.js", "/website_slides/static/src/js/public/components/slide_share_dialog/slide_share_dialog.js", "/website_slides/static/src/js/public/components/slide_unsubscribe_dialog/slide_unsubscribe_dialog.js", "/website_slides/static/src/js/public/components/slide_upload_dialog/slide_install_module.js", "/website_slides/static/src/js/public/components/slide_upload_dialog/slide_upload_category.js", "/website_slides/static/src/js/public/components/slide_upload_dialog/slide_upload_dialog.js", "/website_slides/static/src/js/public/components/slide_upload_dialog/slide_upload_dialog_select.js", "/website_slides/static/src/js/public/components/slide_upload_dialog/slide_upload_select_tags.js", "/website_slides/static/src/js/public/components/slide_upload_dialog/slide_upload_source_types.js", "/planning/static/src/js/planning_calendar_front.js", "/event/static/src/js/tours/event_steps.js", "/event/static/src/js/tours/event_tour.js", "/website_event/static/src/js/tours/event_tour.js", "/website_event/static/src/js/display_timer_widget.js", "/website_event/static/src/js/register_toaster_widget.js", "/website_event/static/src/js/website_event.js", "/website_event/static/src/js/website_event_ticket_details.js", "/web/static/src/views/fields/file_handler.js", "/web/static/src/views/fields/formatters.js", "/mail/static/src/model/export.js", "/mail/static/src/model/make_store.js", "/mail/static/src/model/misc.js", "/mail/static/src/model/model_internal.js", "/mail/static/src/model/record.js", "/mail/static/src/model/record_internal.js", "/mail/static/src/model/record_list.js", "/mail/static/src/model/record_uses.js", "/mail/static/src/model/store.js", "/mail/static/src/model/store_internal.js", "/mail/static/src/core/common/attachment_list.js", "/mail/static/src/core/common/attachment_model.js", "/mail/static/src/core/common/attachment_upload_service.js", "/mail/static/src/core/common/attachment_uploader_hook.js", "/mail/static/src/core/common/attachment_view.js", "/mail/static/src/core/common/autoresize_input.js", "/mail/static/src/core/common/canned_response_model.js", "/mail/static/src/core/common/channel_member_model.js", "/mail/static/src/core/common/chat_bubble.js", "/mail/static/src/core/common/chat_hub.js", "/mail/static/src/core/common/chat_hub_model.js", "/mail/static/src/core/common/chat_window.js", "/mail/static/src/core/common/chat_window_model.js", "/mail/static/src/core/common/composer.js", "/mail/static/src/core/common/composer_model.js", "/mail/static/src/core/common/country_flag.js", "/mail/static/src/core/common/country_model.js", "/mail/static/src/core/common/date_section.js", "/mail/static/src/core/common/discuss_component_registry.js", "/mail/static/src/core/common/emoji_picker_mobile.js", "/mail/static/src/core/common/failure_model.js", "/mail/static/src/core/common/follower_model.js", "/mail/static/src/core/common/im_status.js", "/mail/static/src/core/common/im_status_service_patch.js", "/mail/static/src/core/common/link_preview.js", "/mail/static/src/core/common/link_preview_confirm_delete.js", "/mail/static/src/core/common/link_preview_list.js", "/mail/static/src/core/common/link_preview_model.js", "/mail/static/src/core/common/mail_core_common_service.js", "/mail/static/src/core/common/mail_popout_service.js", "/mail/static/src/core/common/message.js", "/mail/static/src/core/common/message_action_menu_mobile.js", "/mail/static/src/core/common/message_actions.js", "/mail/static/src/core/common/message_card_list.js", "/mail/static/src/core/common/message_confirm_dialog.js", "/mail/static/src/core/common/message_in_reply.js", "/mail/static/src/core/common/message_model.js", "/mail/static/src/core/common/message_notification_popover.js", "/mail/static/src/core/common/message_reaction_button.js", "/mail/static/src/core/common/message_reaction_list.js", "/mail/static/src/core/common/message_reaction_menu.js", "/mail/static/src/core/common/message_reactions.js", "/mail/static/src/core/common/message_reactions_model.js", "/mail/static/src/core/common/message_search_hook.js", "/mail/static/src/core/common/message_seen_indicator.js", "/mail/static/src/core/common/navigable_list.js", "/mail/static/src/core/common/notification_model.js", "/mail/static/src/core/common/notification_permission_service.js", "/mail/static/src/core/common/out_of_focus_service.js", "/mail/static/src/core/common/partner_compare.js", "/mail/static/src/core/common/persona_model.js", "/mail/static/src/core/common/picker.js", "/mail/static/src/core/common/picker_content.js", "/mail/static/src/core/common/record.js", "/mail/static/src/core/common/relative_time.js", "/mail/static/src/core/common/search_messages_panel.js", "/mail/static/src/core/common/settings_model.js", "/mail/static/src/core/common/sound_effects_service.js", "/mail/static/src/core/common/store_service.js", "/mail/static/src/core/common/suggestion_hook.js", "/mail/static/src/core/common/suggestion_service.js", "/mail/static/src/core/common/thread.js", "/mail/static/src/core/common/thread_actions.js", "/mail/static/src/core/common/thread_icon.js", "/mail/static/src/core/common/thread_model.js", "/mail/static/src/core/common/volume_model.js", "/mail/static/src/discuss/core/common/action_panel.js", "/mail/static/src/discuss/core/common/attachment_model_patch.js", "/mail/static/src/discuss/core/common/attachment_panel.js", "/mail/static/src/discuss/core/common/attachment_upload_service_patch.js", "/mail/static/src/discuss/core/common/channel_commands.js", "/mail/static/src/discuss/core/common/channel_invitation.js", "/mail/static/src/discuss/core/common/channel_member_list.js", "/mail/static/src/discuss/core/common/composer_patch.js", "/mail/static/src/discuss/core/common/discuss_core_common_service.js", "/mail/static/src/discuss/core/common/discuss_notification_settings.js", "/mail/static/src/discuss/core/common/discuss_notification_settings_client_action.js", "/mail/static/src/discuss/core/common/message_actions.js", "/mail/static/src/discuss/core/common/message_model_patch.js", "/mail/static/src/discuss/core/common/notification_settings.js", "/mail/static/src/discuss/core/common/partner_compare.js", "/mail/static/src/discuss/core/common/store_service_patch.js", "/mail/static/src/discuss/core/common/suggestion_service_patch.js", "/mail/static/src/discuss/core/common/thread_actions.js", "/mail/static/src/discuss/core/common/thread_model_patch.js", "/mail/static/src/discuss/call/common/blur_manager.js", "/mail/static/src/discuss/call/common/call.js", "/mail/static/src/discuss/call/common/call_action_list.js", "/mail/static/src/discuss/call/common/call_actions.js", "/mail/static/src/discuss/call/common/call_context_menu.js", "/mail/static/src/discuss/call/common/call_invitation.js", "/mail/static/src/discuss/call/common/call_invitations.js", "/mail/static/src/discuss/call/common/call_menu.js", "/mail/static/src/discuss/call/common/call_participant_card.js", "/mail/static/src/discuss/call/common/call_participant_video.js", "/mail/static/src/discuss/call/common/call_settings.js", "/mail/static/src/discuss/call/common/channel_member_patch.js", "/mail/static/src/discuss/call/common/chat_window_patch.js", "/mail/static/src/discuss/call/common/discuss_call_settings_client_action.js", "/mail/static/src/discuss/call/common/discuss_p2p_service.js", "/mail/static/src/discuss/call/common/media_monitoring.js", "/mail/static/src/discuss/call/common/peer_to_peer.js", "/mail/static/src/discuss/call/common/ptt_ad_banner.js", "/mail/static/src/discuss/call/common/ptt_extension_service.js", "/mail/static/src/discuss/call/common/rtc_service.js", "/mail/static/src/discuss/call/common/rtc_session_model.js", "/mail/static/src/discuss/call/common/settings_model_patch.js", "/mail/static/src/discuss/call/common/store_service_patch.js", "/mail/static/src/discuss/call/common/thread_actions.js", "/mail/static/src/discuss/call/common/thread_model_patch.js", "/mail/static/src/discuss/typing/common/composer_patch.js", "/mail/static/src/discuss/typing/common/thread_icon_patch.js", "/mail/static/src/discuss/typing/common/typing.js", "/mail/static/src/utils/common/dates.js", "/mail/static/src/utils/common/format.js", "/mail/static/src/utils/common/hooks.js", "/mail/static/src/utils/common/misc.js", "/rating/static/src/core/common/message_model_patch.js", "/rating/static/src/core/common/rating_model.js", "/im_livechat/static/src/core/common/channel_member_model_patch.js", "/im_livechat/static/src/core/common/livechat_channel_model.js", "/im_livechat/static/src/core/common/message_patch.js", "/im_livechat/static/src/core/common/thread_model_patch.js", "/im_livechat/static/src/embed/common/attachment_upload_service_patch.js", "/im_livechat/static/src/embed/common/autopopup_service.js", "/im_livechat/static/src/embed/common/boot_helpers.js", "/im_livechat/static/src/embed/common/chat_window_model_patch.js", "/im_livechat/static/src/embed/common/chat_window_patch.js", "/im_livechat/static/src/embed/common/chatbot/chatbot_model.js", "/im_livechat/static/src/embed/common/chatbot/chatbot_script_model.js", "/im_livechat/static/src/embed/common/chatbot/chatbot_script_step_answer_model.js", "/im_livechat/static/src/embed/common/chatbot/chatbot_script_step_model.js", "/im_livechat/static/src/embed/common/chatbot/chatbot_service.js", "/im_livechat/static/src/embed/common/chatbot/chatbot_step_model.js", "/im_livechat/static/src/embed/common/close_confirmation.js", "/im_livechat/static/src/embed/common/composer_patch.js", "/im_livechat/static/src/embed/common/disabled_features.js", "/im_livechat/static/src/embed/common/expirable_storage.js", "/im_livechat/static/src/embed/common/feedback_panel/feedback_panel.js", "/im_livechat/static/src/embed/common/feedback_panel/transcript_sender.js", "/im_livechat/static/src/embed/common/history_service.js", "/im_livechat/static/src/embed/common/livechat_button.js", "/im_livechat/static/src/embed/common/livechat_initialized_service.js", "/im_livechat/static/src/embed/common/livechat_rule_model.js", "/im_livechat/static/src/embed/common/livechat_service.js", "/im_livechat/static/src/embed/common/message_model_patch.js", "/im_livechat/static/src/embed/common/message_patch.js", "/im_livechat/static/src/embed/common/misc.js", "/im_livechat/static/src/embed/common/store_service_patch.js", "/im_livechat/static/src/embed/common/thread_actions.js", "/im_livechat/static/src/embed/common/thread_model_patch.js", "/im_livechat/static/src/embed/common/thread_patch.js", "/im_livechat/static/src/embed/frontend/boot_service.js", "/im_livechat/static/src/embed/frontend/livechat_root.js", "/appointment/static/src/js/utils.js", "/appointment/static/src/js/appointment_select_appointment_type.js", "/appointment/static/src/js/appointment_select_appointment_slot.js", "/appointment/static/src/js/appointment_validation.js", "/appointment/static/src/js/appointment_form.js", "/survey/static/src/js/tours/survey_tour.js", "/project_forecast/static/src/js/forecast_calendar_front.js", "/website_helpdesk/static/src/js/website_helpdesk.autocomplete.js", "/website_forum/static/src/js/tours/website_forum.js", "/website_forum/static/src/js/website_forum.js", "/website_forum/static/src/js/website_forum.share.js", "/website_forum/static/src/components/flag_mark_as_offensive/flag_mark_as_offensive.js", "/website_helpdesk_forum/static/src/components/create_ticket_dialog/create_ticket_dialog.js", "/website_helpdesk_forum/static/src/js/forum_create_ticket.js", "/account_online_synchronization/static/src/js/online_sync_portal.js", "/auth_totp_portal/static/src/js/totp_frontend.js", "/website_event_track/static/src/js/website_event_track.js", "/website_event_track/static/src/js/website_event_track_proposal_form.js", "/website_event_track/static/src/js/website_event_track_proposal_form_tags.js", "/website_event_track/static/src/js/event_track_reminder.js", "/website_event_track/static/src/js/event_track_timer.js", "/website_event_track/static/src/js/website_event_pwa_widget.js", "/website_event_track/static/lib/idb-keyval/idb-keyval.js", "/sale_planning/static/src/js/frontend/sale_planning_calendar_portal.js", "/saas_trial/static/js/activation_frontend.js", "/saas_website/static/src/js/db_activation_reminder_frontend.js", "/saas_website/static/src/js/saas_website_frontend.js", "/website_sale_slides/static/src/js/slides_course_join.js", "/website_sale_slides/static/src/js/slides_course_quiz.js", "/website_appointment_sale/static/src/js/appointment_sale_confirmation.js", "/website_cf_turnstile/static/src/js/turnstile.js", "/website_cf_turnstile/static/src/js/error_handler.js", "/website_event_booth/static/src/js/booth_register.js", "/website_jitsi/static/src/js/chat_room.js", "/website_event_exhibitor/static/src/js/event_exhibitor_connect.js", "/website_event_exhibitor/static/src/components/exhibitor_connect_closed_dialog/exhibitor_connect_closed_dialog.js", "/website_event_booth_exhibitor/static/src/js/booth_sponsor_details.js", "/website_event_booth_sale/static/src/js/booth_register.js", "/website_event_track_live/static/src/js/website_event_track_replay_suggestion.js", "/website_event_track_live/static/src/js/website_event_track_suggestion.js", "/website_event_track_live/static/src/js/website_event_track_live.js", "/website_event_track_quiz/static/src/js/event_quiz.js", "/website_event_track_quiz/static/src/js/event_quiz_leaderboard.js", "/website_event_track_live_quiz/static/src/js/event_quiz.js", "/website_event_track_live_quiz/static/src/js/website_event_track_suggestion.js", "/html_editor/static/src/fields/html_viewer.js", "/html_editor/static/src/others/embedded_component_utils.js", "/html_editor/static/src/others/embedded_components/core/embedded_component_toolbar/embedded_component_toolbar.js", "/html_editor/static/src/others/embedded_components/core/excalidraw/excalidraw_utils.js", "/html_editor/static/src/others/embedded_components/core/excalidraw/readonly_excalidraw.js", "/html_editor/static/src/others/embedded_components/core/file/readonly_file.js", "/html_editor/static/src/others/embedded_components/core/file/state_file_model.js", "/html_editor/static/src/others/embedded_components/core/table_of_content/table_of_content.js", "/html_editor/static/src/others/embedded_components/core/table_of_content/table_of_content_manager.js", "/html_editor/static/src/others/embedded_components/core/video/video.js", "/knowledge/static/src/editor/embedded_components/core/article_index/article_index_list.js", "/knowledge/static/src/editor/embedded_components/core/article_index/readonly_article_index.js", "/knowledge/static/src/editor/embedded_components/core/clipboard/embedded_clipboard.js", "/knowledge/static/src/editor/embedded_components/core/embedded_view_link/embedded_view_link_style.js", "/knowledge/static/src/editor/html_viewer/html_viewer_patch.js", "/knowledge/static/src/editor/html_migrations/html_upgrade_manager.js", "/knowledge/static/src/editor/html_migrations/manifest.js", "/knowledge/static/src/editor/html_migrations/migration-1.0.js", "/knowledge/static/src/editor/html_migrations/utils.js", "/website_knowledge/static/src/frontend/editor/embedded_components/embedding_sets.js", "/website_knowledge/static/src/frontend/editor/embedded_components/view/view_placeholder.js", "/website_knowledge/static/src/frontend/editor/embedded_components/view_link/public_embedded_view_link.js", "/website_knowledge/static/src/frontend/editor/html_viewer/public_html_viewer.js", "/website_knowledge/static/src/frontend/knowledge_public_view/knowledge_public_view.js", "/website_links/static/src/js/website_links.js", "/website_links/static/src/js/website_links_code_editor.js", "/website_links/static/src/js/website_links_charts.js", "/website_mass_mailing/static/src/js/website_mass_mailing.js", "/website_sale_stock/static/src/js/combo_configurator_dialog/combo_configurator_dialog.js", "/website_sale_stock/static/src/js/models/product_product.js", "/website_sale_stock/static/src/js/product/product.js", "/website_sale_stock/static/src/js/product_card/product_card.js", "/website_sale_stock/static/src/js/product_configurator_dialog/product_configurator_dialog.js", "/website_sale_stock/static/src/js/variant_mixin.js", "/website_sale_stock/static/src/js/website_sale.js", "/website_sale_stock/static/src/js/website_sale_reorder.js", "/website_sale_wishlist/static/src/js/website_sale.js", "/website_sale_wishlist/static/src/js/website_sale_wishlist.js", "/website_sale_stock_wishlist/static/src/js/variant.js", "/website_sale_stock_wishlist/static/src/js/website_sale.js", "/website_sale_stock_wishlist/static/src/js/wishlist.js", "/website_slides_survey/static/src/js/slides_course_fullscreen_player.js", "/website_slides_survey/static/src/js/public/components/slide_upload_dialog/slide_upload_category.js", "/website_slides_survey/static/src/js/public/components/slide_upload_dialog/slide_upload_dialog.js", "/website_blog/static/src/js/contentshare.js", "/website_blog/static/src/js/website_blog.js", "/website_event_meet/static/src/js/website_event_meeting_room.js", "/website_event_meet/static/src/js/website_event_create_meeting_room_button.js", "/website/static/src/snippets/s_instagram_page/000.js", "/website/static/src/snippets/s_share/000.js", "/website/static/src/snippets/s_facebook_page/000.js", "/website/static/src/snippets/s_image_gallery/000.js", "/website/static/src/snippets/s_countdown/000.js", "/website/static/src/snippets/s_popup/000.js", "/website/static/src/snippets/s_table_of_content/000.js", "/website/static/src/snippets/s_chart/000.js", "/website/static/src/snippets/s_faq_horizontal/000.js", "/website/static/src/snippets/s_google_map/000.js", "/website/static/src/snippets/s_map/000.js", "/website/static/src/snippets/s_dynamic_snippet/000.js", "/website/static/src/snippets/s_dynamic_snippet_carousel/000.js", "/website/static/src/snippets/s_embed_code/000.js", "/website/static/src/snippets/s_website_form/000.js", "/website/static/src/snippets/s_searchbar/000.js", "/website_appointment/static/src/snippets/s_appointments/000.js", "/website_appointment/static/src/snippets/s_online_appointment/000.js", "/website_appointment/static/src/snippets/s_searchbar/000.js", "/website_payment/static/src/snippets/s_donation/000.js", "/website_sale/static/src/snippets/s_add_to_cart/000.js", "/website_sale/static/src/snippets/s_dynamic_snippet_products/000.js", "/website_sale/static/src/snippets/s_popup/000.js", "/website/static/src/js/content/ripple_effect.js", "/website_blog/static/src/snippets/s_blog_posts/000.js", "/website_event/static/src/snippets/s_events/000.js", "/website_event/static/src/snippets/s_searchbar/000.js", "/website_mass_mailing/static/src/snippets/s_popup/000.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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/iPA;;;;;AAAA;AACA;AACA;AACA;AACA;ACJA;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACl7LA;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChvVA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjyDA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzRA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxGA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC3IA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpYA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzPA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChUA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACtPA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACliBA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC7RA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5JA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjOA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3aA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;;;;AChNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AC/YA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5tBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACxDA;;;;;;;;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;;;;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;;;;AC/FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;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;;;;ACtCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACtDA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7XA;;;;;;;;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;;;;ACxOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/JA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;ACzGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1PA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzmBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AChFA;;;;;;;;AAAA;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;;;;AC3EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9TA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1SA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7jBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC5HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3UA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvOA;;;;;;;;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;;;;AClLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC73BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxYA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5TA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7MA;;;;;;;;AAAA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrUA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9KA;;;;;;;;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;;;;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;;;;ACtOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3JA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9kCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/QA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC/JA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AClRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1QA;;;;;;;;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;;;;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;;;;ACjDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpjCA;;;;;;;;AAAA;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;;;;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;;;;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;;;;AClRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9YA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvVA;;;;;;;;AAAA;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;;;;AChGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrWA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5YA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;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;;;;AClVA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/4BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1QA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC5gBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AClWA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/MA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7QA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACx3DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACp8EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACzLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC5LA;;;;;;;;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;;;;AC/OA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtjBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnWA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxlBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC9BA;;;;;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9QA;;;;;;;;AAAA;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;;;;ACnEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7YA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClaA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClLA;;;;;;;;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;;;;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;;;;ACnNA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClWA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1hBA;;;;;;;;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;;;;ACjDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChvGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACnEA;;;;;;;;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;;;;AC3KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AC7FA;;;;;;;;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;;;;ACvEA;;;;;;;;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;;;;ACvGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC35BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC5oBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrmBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC/BA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjFA;;;;;;;;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;;;;AC3GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACngBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxjBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7PA;;;;;;;;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;;;;ACx5BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/lEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;AACA;AACA;;;;ACnJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnOA;;;;;;;;AAAA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnlBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACn3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AC/SA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;;;;AChCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;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;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACtJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;ACpDA;;;;;;;;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;;;;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;;;;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;;;;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;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;AC7WA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/0BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvUA;;;;;;;;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;;;;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;;;;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;AACA;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;;;;ACnEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC9GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;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;;;;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;;;;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;;;;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;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9uBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/xBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AC7BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;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;;;;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;;;;ACjFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/aA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9NA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACxGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACvdA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7PA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3mBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9MA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9JA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;AC3IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;AACA;;;;AC/FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;;;;ACHA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;;;;AC1CA;;;;;;;;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;;;;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;;;;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;;;;ACjDA;;;;;;;;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;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;;;;ACnEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrfA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;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;;;;AC5EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACjKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjBA;;;;;;;;AAAA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/NA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACplBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpkCA;;;;;;;;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;;;;AC3BA;;;;;;;;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;;;;ACnEA;;;;;;;;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;;;;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;;;;ACpIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;AClBA;;;;;;;;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;;;;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;;;;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;;;;AC1CA;;;;;;;;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;;;;ACjFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC7IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AChCA;;;;;;;;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;;;;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;;;;ACzOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;;;;ACJA;;;;;;;;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;;;;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;;;;ACrNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC31BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AChHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACr5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrFA;;;;;;;;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;;;;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;;;;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;;;;AC3RA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5eA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACXA;;;;;;;;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;;;;ACdA;;;;;;;;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;;;;ACfA;;;;;;;;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;;;;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;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;;;;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;;;;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;;;;ACtMA;;;;;;;;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;;;;ACdA;;;;;;;;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;;;;ACrKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACnBA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACXA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/QA;;;;;;;;AAAA;AACA;AACA;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;;;;ACxBA;;;;;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;ACxBA;;;;;;;;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;;;;AC3EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;ACvGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7sBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACnEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/JA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5MA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjFA;;;;;;;;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;AChJA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC3CA;;;;;;;;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;;;;AC7BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACjFA;;;;;;;;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;;;;ACjPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1QA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;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;;;;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;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;;;;AC3UA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACjDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9lBA;;;;;;;;AAAA;AACA;AACA;AACA;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;;;;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;;;;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;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;;;;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;;;;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;;;;AClCA;;;;;;;;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;;;;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;;;;AC/DA;;;;;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;;;;AC1BA;;;;;;;;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;;;;AClRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5YA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACtKA;;;;;;;;AAAA;AACA;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;;;;ACvBA;;;;;;;;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;;;;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;;;;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;;;AC/GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;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;;;;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;;;;ACxSA;;;;;;;;AAAA;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;;;;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;;;;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;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AClNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjmBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9PA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9JA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC/FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACn5BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACjCA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AC7MA;;;;;;;;AAAA;AACA;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;;;;ACnEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACtCA;;;;;;;;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", "sourcesContent": ["var luxon = (function (exports) {\n  'use strict';\n\n  // these aren't really private, but nor are they really useful to document\n\n  /**\n   * @private\n   */\n  class LuxonError extends Error {}\n\n  /**\n   * @private\n   */\n  class InvalidDateTimeError extends LuxonError {\n    constructor(reason) {\n      super(`Invalid DateTime: ${reason.toMessage()}`);\n    }\n  }\n\n  /**\n   * @private\n   */\n  class InvalidIntervalError extends LuxonError {\n    constructor(reason) {\n      super(`Invalid Interval: ${reason.toMessage()}`);\n    }\n  }\n\n  /**\n   * @private\n   */\n  class InvalidDurationError extends LuxonError {\n    constructor(reason) {\n      super(`Invalid Duration: ${reason.toMessage()}`);\n    }\n  }\n\n  /**\n   * @private\n   */\n  class ConflictingSpecificationError extends LuxonError {}\n\n  /**\n   * @private\n   */\n  class InvalidUnitError extends LuxonError {\n    constructor(unit) {\n      super(`Invalid unit ${unit}`);\n    }\n  }\n\n  /**\n   * @private\n   */\n  class InvalidArgumentError extends LuxonError {}\n\n  /**\n   * @private\n   */\n  class ZoneIsAbstractError extends LuxonError {\n    constructor() {\n      super(\"Zone is an abstract class\");\n    }\n  }\n\n  /**\n   * @private\n   */\n\n  const n = \"numeric\",\n    s = \"short\",\n    l = \"long\";\n\n  const DATE_SHORT = {\n    year: n,\n    month: n,\n    day: n,\n  };\n\n  const DATE_MED = {\n    year: n,\n    month: s,\n    day: n,\n  };\n\n  const DATE_MED_WITH_WEEKDAY = {\n    year: n,\n    month: s,\n    day: n,\n    weekday: s,\n  };\n\n  const DATE_FULL = {\n    year: n,\n    month: l,\n    day: n,\n  };\n\n  const DATE_HUGE = {\n    year: n,\n    month: l,\n    day: n,\n    weekday: l,\n  };\n\n  const TIME_SIMPLE = {\n    hour: n,\n    minute: n,\n  };\n\n  const TIME_WITH_SECONDS = {\n    hour: n,\n    minute: n,\n    second: n,\n  };\n\n  const TIME_WITH_SHORT_OFFSET = {\n    hour: n,\n    minute: n,\n    second: n,\n    timeZoneName: s,\n  };\n\n  const TIME_WITH_LONG_OFFSET = {\n    hour: n,\n    minute: n,\n    second: n,\n    timeZoneName: l,\n  };\n\n  const TIME_24_SIMPLE = {\n    hour: n,\n    minute: n,\n    hourCycle: \"h23\",\n  };\n\n  const TIME_24_WITH_SECONDS = {\n    hour: n,\n    minute: n,\n    second: n,\n    hourCycle: \"h23\",\n  };\n\n  const TIME_24_WITH_SHORT_OFFSET = {\n    hour: n,\n    minute: n,\n    second: n,\n    hourCycle: \"h23\",\n    timeZoneName: s,\n  };\n\n  const TIME_24_WITH_LONG_OFFSET = {\n    hour: n,\n    minute: n,\n    second: n,\n    hourCycle: \"h23\",\n    timeZoneName: l,\n  };\n\n  const DATETIME_SHORT = {\n    year: n,\n    month: n,\n    day: n,\n    hour: n,\n    minute: n,\n  };\n\n  const DATETIME_SHORT_WITH_SECONDS = {\n    year: n,\n    month: n,\n    day: n,\n    hour: n,\n    minute: n,\n    second: n,\n  };\n\n  const DATETIME_MED = {\n    year: n,\n    month: s,\n    day: n,\n    hour: n,\n    minute: n,\n  };\n\n  const DATETIME_MED_WITH_SECONDS = {\n    year: n,\n    month: s,\n    day: n,\n    hour: n,\n    minute: n,\n    second: n,\n  };\n\n  const DATETIME_MED_WITH_WEEKDAY = {\n    year: n,\n    month: s,\n    day: n,\n    weekday: s,\n    hour: n,\n    minute: n,\n  };\n\n  const DATETIME_FULL = {\n    year: n,\n    month: l,\n    day: n,\n    hour: n,\n    minute: n,\n    timeZoneName: s,\n  };\n\n  const DATETIME_FULL_WITH_SECONDS = {\n    year: n,\n    month: l,\n    day: n,\n    hour: n,\n    minute: n,\n    second: n,\n    timeZoneName: s,\n  };\n\n  const DATETIME_HUGE = {\n    year: n,\n    month: l,\n    day: n,\n    weekday: l,\n    hour: n,\n    minute: n,\n    timeZoneName: l,\n  };\n\n  const DATETIME_HUGE_WITH_SECONDS = {\n    year: n,\n    month: l,\n    day: n,\n    weekday: l,\n    hour: n,\n    minute: n,\n    second: n,\n    timeZoneName: l,\n  };\n\n  /**\n   * @interface\n   */\n  class Zone {\n    /**\n     * The type of zone\n     * @abstract\n     * @type {string}\n     */\n    get type() {\n      throw new ZoneIsAbstractError();\n    }\n\n    /**\n     * The name of this zone.\n     * @abstract\n     * @type {string}\n     */\n    get name() {\n      throw new ZoneIsAbstractError();\n    }\n\n    get ianaName() {\n      return this.name;\n    }\n\n    /**\n     * Returns whether the offset is known to be fixed for the whole year.\n     * @abstract\n     * @type {boolean}\n     */\n    get isUniversal() {\n      throw new ZoneIsAbstractError();\n    }\n\n    /**\n     * Returns the offset's common name (such as EST) at the specified timestamp\n     * @abstract\n     * @param {number} ts - Epoch milliseconds for which to get the name\n     * @param {Object} opts - Options to affect the format\n     * @param {string} opts.format - What style of offset to return. Accepts 'long' or 'short'.\n     * @param {string} opts.locale - What locale to return the offset name in.\n     * @return {string}\n     */\n    offsetName(ts, opts) {\n      throw new ZoneIsAbstractError();\n    }\n\n    /**\n     * Returns the offset's value as a string\n     * @abstract\n     * @param {number} ts - Epoch milliseconds for which to get the offset\n     * @param {string} format - What style of offset to return.\n     *                          Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively\n     * @return {string}\n     */\n    formatOffset(ts, format) {\n      throw new ZoneIsAbstractError();\n    }\n\n    /**\n     * Return the offset in minutes for this zone at the specified timestamp.\n     * @abstract\n     * @param {number} ts - Epoch milliseconds for which to compute the offset\n     * @return {number}\n     */\n    offset(ts) {\n      throw new ZoneIsAbstractError();\n    }\n\n    /**\n     * Return whether this Zone is equal to another zone\n     * @abstract\n     * @param {Zone} otherZone - the zone to compare\n     * @return {boolean}\n     */\n    equals(otherZone) {\n      throw new ZoneIsAbstractError();\n    }\n\n    /**\n     * Return whether this Zone is valid.\n     * @abstract\n     * @type {boolean}\n     */\n    get isValid() {\n      throw new ZoneIsAbstractError();\n    }\n  }\n\n  let singleton$1 = null;\n\n  /**\n   * Represents the local zone for this JavaScript environment.\n   * @implements {Zone}\n   */\n  class SystemZone extends Zone {\n    /**\n     * Get a singleton instance of the local zone\n     * @return {SystemZone}\n     */\n    static get instance() {\n      if (singleton$1 === null) {\n        singleton$1 = new SystemZone();\n      }\n      return singleton$1;\n    }\n\n    /** @override **/\n    get type() {\n      return \"system\";\n    }\n\n    /** @override **/\n    get name() {\n      return new Intl.DateTimeFormat().resolvedOptions().timeZone;\n    }\n\n    /** @override **/\n    get isUniversal() {\n      return false;\n    }\n\n    /** @override **/\n    offsetName(ts, { format, locale }) {\n      return parseZoneInfo(ts, format, locale);\n    }\n\n    /** @override **/\n    formatOffset(ts, format) {\n      return formatOffset(this.offset(ts), format);\n    }\n\n    /** @override **/\n    offset(ts) {\n      return -new Date(ts).getTimezoneOffset();\n    }\n\n    /** @override **/\n    equals(otherZone) {\n      return otherZone.type === \"system\";\n    }\n\n    /** @override **/\n    get isValid() {\n      return true;\n    }\n  }\n\n  let dtfCache = {};\n  function makeDTF(zone) {\n    if (!dtfCache[zone]) {\n      dtfCache[zone] = new Intl.DateTimeFormat(\"en-US\", {\n        hour12: false,\n        timeZone: zone,\n        year: \"numeric\",\n        month: \"2-digit\",\n        day: \"2-digit\",\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n        second: \"2-digit\",\n        era: \"short\",\n      });\n    }\n    return dtfCache[zone];\n  }\n\n  const typeToPos = {\n    year: 0,\n    month: 1,\n    day: 2,\n    era: 3,\n    hour: 4,\n    minute: 5,\n    second: 6,\n  };\n\n  function hackyOffset(dtf, date) {\n    const formatted = dtf.format(date).replace(/\\u200E/g, \"\"),\n      parsed = /(\\d+)\\/(\\d+)\\/(\\d+) (AD|BC),? (\\d+):(\\d+):(\\d+)/.exec(formatted),\n      [, fMonth, fDay, fYear, fadOrBc, fHour, fMinute, fSecond] = parsed;\n    return [fYear, fMonth, fDay, fadOrBc, fHour, fMinute, fSecond];\n  }\n\n  function partsOffset(dtf, date) {\n    const formatted = dtf.formatToParts(date);\n    const filled = [];\n    for (let i = 0; i < formatted.length; i++) {\n      const { type, value } = formatted[i];\n      const pos = typeToPos[type];\n\n      if (type === \"era\") {\n        filled[pos] = value;\n      } else if (!isUndefined(pos)) {\n        filled[pos] = parseInt(value, 10);\n      }\n    }\n    return filled;\n  }\n\n  let ianaZoneCache = {};\n  /**\n   * A zone identified by an IANA identifier, like America/New_York\n   * @implements {Zone}\n   */\n  class IANAZone extends Zone {\n    /**\n     * @param {string} name - Zone name\n     * @return {IANAZone}\n     */\n    static create(name) {\n      if (!ianaZoneCache[name]) {\n        ianaZoneCache[name] = new IANAZone(name);\n      }\n      return ianaZoneCache[name];\n    }\n\n    /**\n     * Reset local caches. Should only be necessary in testing scenarios.\n     * @return {void}\n     */\n    static resetCache() {\n      ianaZoneCache = {};\n      dtfCache = {};\n    }\n\n    /**\n     * Returns whether the provided string is a valid specifier. This only checks the string's format, not that the specifier identifies a known zone; see isValidZone for that.\n     * @param {string} s - The string to check validity on\n     * @example IANAZone.isValidSpecifier(\"America/New_York\") //=> true\n     * @example IANAZone.isValidSpecifier(\"Sport~~blorp\") //=> false\n     * @deprecated This method returns false for some valid IANA names. Use isValidZone instead.\n     * @return {boolean}\n     */\n    static isValidSpecifier(s) {\n      return this.isValidZone(s);\n    }\n\n    /**\n     * Returns whether the provided string identifies a real zone\n     * @param {string} zone - The string to check\n     * @example IANAZone.isValidZone(\"America/New_York\") //=> true\n     * @example IANAZone.isValidZone(\"Fantasia/Castle\") //=> false\n     * @example IANAZone.isValidZone(\"Sport~~blorp\") //=> false\n     * @return {boolean}\n     */\n    static isValidZone(zone) {\n      if (!zone) {\n        return false;\n      }\n      try {\n        new Intl.DateTimeFormat(\"en-US\", { timeZone: zone }).format();\n        return true;\n      } catch (e) {\n        return false;\n      }\n    }\n\n    constructor(name) {\n      super();\n      /** @private **/\n      this.zoneName = name;\n      /** @private **/\n      this.valid = IANAZone.isValidZone(name);\n    }\n\n    /** @override **/\n    get type() {\n      return \"iana\";\n    }\n\n    /** @override **/\n    get name() {\n      return this.zoneName;\n    }\n\n    /** @override **/\n    get isUniversal() {\n      return false;\n    }\n\n    /** @override **/\n    offsetName(ts, { format, locale }) {\n      return parseZoneInfo(ts, format, locale, this.name);\n    }\n\n    /** @override **/\n    formatOffset(ts, format) {\n      return formatOffset(this.offset(ts), format);\n    }\n\n    /** @override **/\n    offset(ts) {\n      const date = new Date(ts);\n\n      if (isNaN(date)) return NaN;\n\n      const dtf = makeDTF(this.name);\n      let [year, month, day, adOrBc, hour, minute, second] = dtf.formatToParts\n        ? partsOffset(dtf, date)\n        : hackyOffset(dtf, date);\n\n      if (adOrBc === \"BC\") {\n        year = -Math.abs(year) + 1;\n      }\n\n      // because we're using hour12 and https://bugs.chromium.org/p/chromium/issues/detail?id=1025564&can=2&q=%2224%3A00%22%20datetimeformat\n      const adjustedHour = hour === 24 ? 0 : hour;\n\n      const asUTC = objToLocalTS({\n        year,\n        month,\n        day,\n        hour: adjustedHour,\n        minute,\n        second,\n        millisecond: 0,\n      });\n\n      let asTS = +date;\n      const over = asTS % 1000;\n      asTS -= over >= 0 ? over : 1000 + over;\n      return (asUTC - asTS) / (60 * 1000);\n    }\n\n    /** @override **/\n    equals(otherZone) {\n      return otherZone.type === \"iana\" && otherZone.name === this.name;\n    }\n\n    /** @override **/\n    get isValid() {\n      return this.valid;\n    }\n  }\n\n  // todo - remap caching\n\n  let intlLFCache = {};\n  function getCachedLF(locString, opts = {}) {\n    const key = JSON.stringify([locString, opts]);\n    let dtf = intlLFCache[key];\n    if (!dtf) {\n      dtf = new Intl.ListFormat(locString, opts);\n      intlLFCache[key] = dtf;\n    }\n    return dtf;\n  }\n\n  let intlDTCache = {};\n  function getCachedDTF(locString, opts = {}) {\n    const key = JSON.stringify([locString, opts]);\n    let dtf = intlDTCache[key];\n    if (!dtf) {\n      dtf = new Intl.DateTimeFormat(locString, opts);\n      intlDTCache[key] = dtf;\n    }\n    return dtf;\n  }\n\n  let intlNumCache = {};\n  function getCachedINF(locString, opts = {}) {\n    const key = JSON.stringify([locString, opts]);\n    let inf = intlNumCache[key];\n    if (!inf) {\n      inf = new Intl.NumberFormat(locString, opts);\n      intlNumCache[key] = inf;\n    }\n    return inf;\n  }\n\n  let intlRelCache = {};\n  function getCachedRTF(locString, opts = {}) {\n    const { base, ...cacheKeyOpts } = opts; // exclude `base` from the options\n    const key = JSON.stringify([locString, cacheKeyOpts]);\n    let inf = intlRelCache[key];\n    if (!inf) {\n      inf = new Intl.RelativeTimeFormat(locString, opts);\n      intlRelCache[key] = inf;\n    }\n    return inf;\n  }\n\n  let sysLocaleCache = null;\n  function systemLocale() {\n    if (sysLocaleCache) {\n      return sysLocaleCache;\n    } else {\n      sysLocaleCache = new Intl.DateTimeFormat().resolvedOptions().locale;\n      return sysLocaleCache;\n    }\n  }\n\n  let weekInfoCache = {};\n  function getCachedWeekInfo(locString) {\n    let data = weekInfoCache[locString];\n    if (!data) {\n      const locale = new Intl.Locale(locString);\n      // browsers currently implement this as a property, but spec says it should be a getter function\n      data = \"getWeekInfo\" in locale ? locale.getWeekInfo() : locale.weekInfo;\n      weekInfoCache[locString] = data;\n    }\n    return data;\n  }\n\n  function parseLocaleString(localeStr) {\n    // I really want to avoid writing a BCP 47 parser\n    // see, e.g. https://github.com/wooorm/bcp-47\n    // Instead, we'll do this:\n\n    // a) if the string has no -u extensions, just leave it alone\n    // b) if it does, use Intl to resolve everything\n    // c) if Intl fails, try again without the -u\n\n    // private subtags and unicode subtags have ordering requirements,\n    // and we're not properly parsing this, so just strip out the\n    // private ones if they exist.\n    const xIndex = localeStr.indexOf(\"-x-\");\n    if (xIndex !== -1) {\n      localeStr = localeStr.substring(0, xIndex);\n    }\n\n    const uIndex = localeStr.indexOf(\"-u-\");\n    if (uIndex === -1) {\n      return [localeStr];\n    } else {\n      let options;\n      let selectedStr;\n      try {\n        options = getCachedDTF(localeStr).resolvedOptions();\n        selectedStr = localeStr;\n      } catch (e) {\n        const smaller = localeStr.substring(0, uIndex);\n        options = getCachedDTF(smaller).resolvedOptions();\n        selectedStr = smaller;\n      }\n\n      const { numberingSystem, calendar } = options;\n      return [selectedStr, numberingSystem, calendar];\n    }\n  }\n\n  function intlConfigString(localeStr, numberingSystem, outputCalendar) {\n    if (outputCalendar || numberingSystem) {\n      if (!localeStr.includes(\"-u-\")) {\n        localeStr += \"-u\";\n      }\n\n      if (outputCalendar) {\n        localeStr += `-ca-${outputCalendar}`;\n      }\n\n      if (numberingSystem) {\n        localeStr += `-nu-${numberingSystem}`;\n      }\n      return localeStr;\n    } else {\n      return localeStr;\n    }\n  }\n\n  function mapMonths(f) {\n    const ms = [];\n    for (let i = 1; i <= 12; i++) {\n      const dt = DateTime.utc(2009, i, 1);\n      ms.push(f(dt));\n    }\n    return ms;\n  }\n\n  function mapWeekdays(f) {\n    const ms = [];\n    for (let i = 1; i <= 7; i++) {\n      const dt = DateTime.utc(2016, 11, 13 + i);\n      ms.push(f(dt));\n    }\n    return ms;\n  }\n\n  function listStuff(loc, length, englishFn, intlFn) {\n    const mode = loc.listingMode();\n\n    if (mode === \"error\") {\n      return null;\n    } else if (mode === \"en\") {\n      return englishFn(length);\n    } else {\n      return intlFn(length);\n    }\n  }\n\n  function supportsFastNumbers(loc) {\n    if (loc.numberingSystem && loc.numberingSystem !== \"latn\") {\n      return false;\n    } else {\n      return (\n        loc.numberingSystem === \"latn\" ||\n        !loc.locale ||\n        loc.locale.startsWith(\"en\") ||\n        new Intl.DateTimeFormat(loc.intl).resolvedOptions().numberingSystem === \"latn\"\n      );\n    }\n  }\n\n  /**\n   * @private\n   */\n\n  class PolyNumberFormatter {\n    constructor(intl, forceSimple, opts) {\n      this.padTo = opts.padTo || 0;\n      this.floor = opts.floor || false;\n\n      const { padTo, floor, ...otherOpts } = opts;\n\n      if (!forceSimple || Object.keys(otherOpts).length > 0) {\n        const intlOpts = { useGrouping: false, ...opts };\n        if (opts.padTo > 0) intlOpts.minimumIntegerDigits = opts.padTo;\n        this.inf = getCachedINF(intl, intlOpts);\n      }\n    }\n\n    format(i) {\n      if (this.inf) {\n        const fixed = this.floor ? Math.floor(i) : i;\n        return this.inf.format(fixed);\n      } else {\n        // to match the browser's numberformatter defaults\n        const fixed = this.floor ? Math.floor(i) : roundTo(i, 3);\n        return padStart(fixed, this.padTo);\n      }\n    }\n  }\n\n  /**\n   * @private\n   */\n\n  class PolyDateFormatter {\n    constructor(dt, intl, opts) {\n      this.opts = opts;\n      this.originalZone = undefined;\n\n      let z = undefined;\n      if (this.opts.timeZone) {\n        // Don't apply any workarounds if a timeZone is explicitly provided in opts\n        this.dt = dt;\n      } else if (dt.zone.type === \"fixed\") {\n        // UTC-8 or Etc/UTC-8 are not part of tzdata, only Etc/GMT+8 and the like.\n        // That is why fixed-offset TZ is set to that unless it is:\n        // 1. Representing offset 0 when UTC is used to maintain previous behavior and does not become GMT.\n        // 2. Unsupported by the browser:\n        //    - some do not support Etc/\n        //    - < Etc/GMT-14, > Etc/GMT+12, and 30-minute or 45-minute offsets are not part of tzdata\n        const gmtOffset = -1 * (dt.offset / 60);\n        const offsetZ = gmtOffset >= 0 ? `Etc/GMT+${gmtOffset}` : `Etc/GMT${gmtOffset}`;\n        if (dt.offset !== 0 && IANAZone.create(offsetZ).valid) {\n          z = offsetZ;\n          this.dt = dt;\n        } else {\n          // Not all fixed-offset zones like Etc/+4:30 are present in tzdata so\n          // we manually apply the offset and substitute the zone as needed.\n          z = \"UTC\";\n          this.dt = dt.offset === 0 ? dt : dt.setZone(\"UTC\").plus({ minutes: dt.offset });\n          this.originalZone = dt.zone;\n        }\n      } else if (dt.zone.type === \"system\") {\n        this.dt = dt;\n      } else if (dt.zone.type === \"iana\") {\n        this.dt = dt;\n        z = dt.zone.name;\n      } else {\n        // Custom zones can have any offset / offsetName so we just manually\n        // apply the offset and substitute the zone as needed.\n        z = \"UTC\";\n        this.dt = dt.setZone(\"UTC\").plus({ minutes: dt.offset });\n        this.originalZone = dt.zone;\n      }\n\n      const intlOpts = { ...this.opts };\n      intlOpts.timeZone = intlOpts.timeZone || z;\n      this.dtf = getCachedDTF(intl, intlOpts);\n    }\n\n    format() {\n      if (this.originalZone) {\n        // If we have to substitute in the actual zone name, we have to use\n        // formatToParts so that the timezone can be replaced.\n        return this.formatToParts()\n          .map(({ value }) => value)\n          .join(\"\");\n      }\n      return this.dtf.format(this.dt.toJSDate());\n    }\n\n    formatToParts() {\n      const parts = this.dtf.formatToParts(this.dt.toJSDate());\n      if (this.originalZone) {\n        return parts.map((part) => {\n          if (part.type === \"timeZoneName\") {\n            const offsetName = this.originalZone.offsetName(this.dt.ts, {\n              locale: this.dt.locale,\n              format: this.opts.timeZoneName,\n            });\n            return {\n              ...part,\n              value: offsetName,\n            };\n          } else {\n            return part;\n          }\n        });\n      }\n      return parts;\n    }\n\n    resolvedOptions() {\n      return this.dtf.resolvedOptions();\n    }\n  }\n\n  /**\n   * @private\n   */\n  class PolyRelFormatter {\n    constructor(intl, isEnglish, opts) {\n      this.opts = { style: \"long\", ...opts };\n      if (!isEnglish && hasRelative()) {\n        this.rtf = getCachedRTF(intl, opts);\n      }\n    }\n\n    format(count, unit) {\n      if (this.rtf) {\n        return this.rtf.format(count, unit);\n      } else {\n        return formatRelativeTime(unit, count, this.opts.numeric, this.opts.style !== \"long\");\n      }\n    }\n\n    formatToParts(count, unit) {\n      if (this.rtf) {\n        return this.rtf.formatToParts(count, unit);\n      } else {\n        return [];\n      }\n    }\n  }\n\n  const fallbackWeekSettings = {\n    firstDay: 1,\n    minimalDays: 4,\n    weekend: [6, 7],\n  };\n\n  /**\n   * @private\n   */\n\n  class Locale {\n    static fromOpts(opts) {\n      return Locale.create(\n        opts.locale,\n        opts.numberingSystem,\n        opts.outputCalendar,\n        opts.weekSettings,\n        opts.defaultToEN\n      );\n    }\n\n    static create(locale, numberingSystem, outputCalendar, weekSettings, defaultToEN = false) {\n      const specifiedLocale = locale || Settings.defaultLocale;\n      // the system locale is useful for human readable strings but annoying for parsing/formatting known formats\n      const localeR = specifiedLocale || (defaultToEN ? \"en-US\" : systemLocale());\n      const numberingSystemR = numberingSystem || Settings.defaultNumberingSystem;\n      const outputCalendarR = outputCalendar || Settings.defaultOutputCalendar;\n      const weekSettingsR = validateWeekSettings(weekSettings) || Settings.defaultWeekSettings;\n      return new Locale(localeR, numberingSystemR, outputCalendarR, weekSettingsR, specifiedLocale);\n    }\n\n    static resetCache() {\n      sysLocaleCache = null;\n      intlDTCache = {};\n      intlNumCache = {};\n      intlRelCache = {};\n    }\n\n    static fromObject({ locale, numberingSystem, outputCalendar, weekSettings } = {}) {\n      return Locale.create(locale, numberingSystem, outputCalendar, weekSettings);\n    }\n\n    constructor(locale, numbering, outputCalendar, weekSettings, specifiedLocale) {\n      const [parsedLocale, parsedNumberingSystem, parsedOutputCalendar] = parseLocaleString(locale);\n\n      this.locale = parsedLocale;\n      this.numberingSystem = numbering || parsedNumberingSystem || null;\n      this.outputCalendar = outputCalendar || parsedOutputCalendar || null;\n      this.weekSettings = weekSettings;\n      this.intl = intlConfigString(this.locale, this.numberingSystem, this.outputCalendar);\n\n      this.weekdaysCache = { format: {}, standalone: {} };\n      this.monthsCache = { format: {}, standalone: {} };\n      this.meridiemCache = null;\n      this.eraCache = {};\n\n      this.specifiedLocale = specifiedLocale;\n      this.fastNumbersCached = null;\n    }\n\n    get fastNumbers() {\n      if (this.fastNumbersCached == null) {\n        this.fastNumbersCached = supportsFastNumbers(this);\n      }\n\n      return this.fastNumbersCached;\n    }\n\n    listingMode() {\n      const isActuallyEn = this.isEnglish();\n      const hasNoWeirdness =\n        (this.numberingSystem === null || this.numberingSystem === \"latn\") &&\n        (this.outputCalendar === null || this.outputCalendar === \"gregory\");\n      return isActuallyEn && hasNoWeirdness ? \"en\" : \"intl\";\n    }\n\n    clone(alts) {\n      if (!alts || Object.getOwnPropertyNames(alts).length === 0) {\n        return this;\n      } else {\n        return Locale.create(\n          alts.locale || this.specifiedLocale,\n          alts.numberingSystem || this.numberingSystem,\n          alts.outputCalendar || this.outputCalendar,\n          validateWeekSettings(alts.weekSettings) || this.weekSettings,\n          alts.defaultToEN || false\n        );\n      }\n    }\n\n    redefaultToEN(alts = {}) {\n      return this.clone({ ...alts, defaultToEN: true });\n    }\n\n    redefaultToSystem(alts = {}) {\n      return this.clone({ ...alts, defaultToEN: false });\n    }\n\n    months(length, format = false) {\n      return listStuff(this, length, months, () => {\n        const intl = format ? { month: length, day: \"numeric\" } : { month: length },\n          formatStr = format ? \"format\" : \"standalone\";\n        if (!this.monthsCache[formatStr][length]) {\n          this.monthsCache[formatStr][length] = mapMonths((dt) => this.extract(dt, intl, \"month\"));\n        }\n        return this.monthsCache[formatStr][length];\n      });\n    }\n\n    weekdays(length, format = false) {\n      return listStuff(this, length, weekdays, () => {\n        const intl = format\n            ? { weekday: length, year: \"numeric\", month: \"long\", day: \"numeric\" }\n            : { weekday: length },\n          formatStr = format ? \"format\" : \"standalone\";\n        if (!this.weekdaysCache[formatStr][length]) {\n          this.weekdaysCache[formatStr][length] = mapWeekdays((dt) =>\n            this.extract(dt, intl, \"weekday\")\n          );\n        }\n        return this.weekdaysCache[formatStr][length];\n      });\n    }\n\n    meridiems() {\n      return listStuff(\n        this,\n        undefined,\n        () => meridiems,\n        () => {\n          // In theory there could be aribitrary day periods. We're gonna assume there are exactly two\n          // for AM and PM. This is probably wrong, but it's makes parsing way easier.\n          if (!this.meridiemCache) {\n            const intl = { hour: \"numeric\", hourCycle: \"h12\" };\n            this.meridiemCache = [DateTime.utc(2016, 11, 13, 9), DateTime.utc(2016, 11, 13, 19)].map(\n              (dt) => this.extract(dt, intl, \"dayperiod\")\n            );\n          }\n\n          return this.meridiemCache;\n        }\n      );\n    }\n\n    eras(length) {\n      return listStuff(this, length, eras, () => {\n        const intl = { era: length };\n\n        // This is problematic. Different calendars are going to define eras totally differently. What I need is the minimum set of dates\n        // to definitely enumerate them.\n        if (!this.eraCache[length]) {\n          this.eraCache[length] = [DateTime.utc(-40, 1, 1), DateTime.utc(2017, 1, 1)].map((dt) =>\n            this.extract(dt, intl, \"era\")\n          );\n        }\n\n        return this.eraCache[length];\n      });\n    }\n\n    extract(dt, intlOpts, field) {\n      const df = this.dtFormatter(dt, intlOpts),\n        results = df.formatToParts(),\n        matching = results.find((m) => m.type.toLowerCase() === field);\n      return matching ? matching.value : null;\n    }\n\n    numberFormatter(opts = {}) {\n      // this forcesimple option is never used (the only caller short-circuits on it, but it seems safer to leave)\n      // (in contrast, the rest of the condition is used heavily)\n      return new PolyNumberFormatter(this.intl, opts.forceSimple || this.fastNumbers, opts);\n    }\n\n    dtFormatter(dt, intlOpts = {}) {\n      return new PolyDateFormatter(dt, this.intl, intlOpts);\n    }\n\n    relFormatter(opts = {}) {\n      return new PolyRelFormatter(this.intl, this.isEnglish(), opts);\n    }\n\n    listFormatter(opts = {}) {\n      return getCachedLF(this.intl, opts);\n    }\n\n    isEnglish() {\n      return (\n        this.locale === \"en\" ||\n        this.locale.toLowerCase() === \"en-us\" ||\n        new Intl.DateTimeFormat(this.intl).resolvedOptions().locale.startsWith(\"en-us\")\n      );\n    }\n\n    getWeekSettings() {\n      if (this.weekSettings) {\n        return this.weekSettings;\n      } else if (!hasLocaleWeekInfo()) {\n        return fallbackWeekSettings;\n      } else {\n        return getCachedWeekInfo(this.locale);\n      }\n    }\n\n    getStartOfWeek() {\n      return this.getWeekSettings().firstDay;\n    }\n\n    getMinDaysInFirstWeek() {\n      return this.getWeekSettings().minimalDays;\n    }\n\n    getWeekendDays() {\n      return this.getWeekSettings().weekend;\n    }\n\n    equals(other) {\n      return (\n        this.locale === other.locale &&\n        this.numberingSystem === other.numberingSystem &&\n        this.outputCalendar === other.outputCalendar\n      );\n    }\n  }\n\n  let singleton = null;\n\n  /**\n   * A zone with a fixed offset (meaning no DST)\n   * @implements {Zone}\n   */\n  class FixedOffsetZone extends Zone {\n    /**\n     * Get a singleton instance of UTC\n     * @return {FixedOffsetZone}\n     */\n    static get utcInstance() {\n      if (singleton === null) {\n        singleton = new FixedOffsetZone(0);\n      }\n      return singleton;\n    }\n\n    /**\n     * Get an instance with a specified offset\n     * @param {number} offset - The offset in minutes\n     * @return {FixedOffsetZone}\n     */\n    static instance(offset) {\n      return offset === 0 ? FixedOffsetZone.utcInstance : new FixedOffsetZone(offset);\n    }\n\n    /**\n     * Get an instance of FixedOffsetZone from a UTC offset string, like \"UTC+6\"\n     * @param {string} s - The offset string to parse\n     * @example FixedOffsetZone.parseSpecifier(\"UTC+6\")\n     * @example FixedOffsetZone.parseSpecifier(\"UTC+06\")\n     * @example FixedOffsetZone.parseSpecifier(\"UTC-6:00\")\n     * @return {FixedOffsetZone}\n     */\n    static parseSpecifier(s) {\n      if (s) {\n        const r = s.match(/^utc(?:([+-]\\d{1,2})(?::(\\d{2}))?)?$/i);\n        if (r) {\n          return new FixedOffsetZone(signedOffset(r[1], r[2]));\n        }\n      }\n      return null;\n    }\n\n    constructor(offset) {\n      super();\n      /** @private **/\n      this.fixed = offset;\n    }\n\n    /** @override **/\n    get type() {\n      return \"fixed\";\n    }\n\n    /** @override **/\n    get name() {\n      return this.fixed === 0 ? \"UTC\" : `UTC${formatOffset(this.fixed, \"narrow\")}`;\n    }\n\n    get ianaName() {\n      if (this.fixed === 0) {\n        return \"Etc/UTC\";\n      } else {\n        return `Etc/GMT${formatOffset(-this.fixed, \"narrow\")}`;\n      }\n    }\n\n    /** @override **/\n    offsetName() {\n      return this.name;\n    }\n\n    /** @override **/\n    formatOffset(ts, format) {\n      return formatOffset(this.fixed, format);\n    }\n\n    /** @override **/\n    get isUniversal() {\n      return true;\n    }\n\n    /** @override **/\n    offset() {\n      return this.fixed;\n    }\n\n    /** @override **/\n    equals(otherZone) {\n      return otherZone.type === \"fixed\" && otherZone.fixed === this.fixed;\n    }\n\n    /** @override **/\n    get isValid() {\n      return true;\n    }\n  }\n\n  /**\n   * A zone that failed to parse. You should never need to instantiate this.\n   * @implements {Zone}\n   */\n  class InvalidZone extends Zone {\n    constructor(zoneName) {\n      super();\n      /**  @private */\n      this.zoneName = zoneName;\n    }\n\n    /** @override **/\n    get type() {\n      return \"invalid\";\n    }\n\n    /** @override **/\n    get name() {\n      return this.zoneName;\n    }\n\n    /** @override **/\n    get isUniversal() {\n      return false;\n    }\n\n    /** @override **/\n    offsetName() {\n      return null;\n    }\n\n    /** @override **/\n    formatOffset() {\n      return \"\";\n    }\n\n    /** @override **/\n    offset() {\n      return NaN;\n    }\n\n    /** @override **/\n    equals() {\n      return false;\n    }\n\n    /** @override **/\n    get isValid() {\n      return false;\n    }\n  }\n\n  /**\n   * @private\n   */\n\n  function normalizeZone(input, defaultZone) {\n    if (isUndefined(input) || input === null) {\n      return defaultZone;\n    } else if (input instanceof Zone) {\n      return input;\n    } else if (isString(input)) {\n      const lowered = input.toLowerCase();\n      if (lowered === \"default\") return defaultZone;\n      else if (lowered === \"local\" || lowered === \"system\") return SystemZone.instance;\n      else if (lowered === \"utc\" || lowered === \"gmt\") return FixedOffsetZone.utcInstance;\n      else return FixedOffsetZone.parseSpecifier(lowered) || IANAZone.create(input);\n    } else if (isNumber(input)) {\n      return FixedOffsetZone.instance(input);\n    } else if (typeof input === \"object\" && \"offset\" in input && typeof input.offset === \"function\") {\n      // This is dumb, but the instanceof check above doesn't seem to really work\n      // so we're duck checking it\n      return input;\n    } else {\n      return new InvalidZone(input);\n    }\n  }\n\n  let now = () => Date.now(),\n    defaultZone = \"system\",\n    defaultLocale = null,\n    defaultNumberingSystem = null,\n    defaultOutputCalendar = null,\n    twoDigitCutoffYear = 60,\n    throwOnInvalid,\n    defaultWeekSettings = null;\n\n  /**\n   * Settings contains static getters and setters that control Luxon's overall behavior. Luxon is a simple library with few options, but the ones it does have live here.\n   */\n  class Settings {\n    /**\n     * Get the callback for returning the current timestamp.\n     * @type {function}\n     */\n    static get now() {\n      return now;\n    }\n\n    /**\n     * Set the callback for returning the current timestamp.\n     * The function should return a number, which will be interpreted as an Epoch millisecond count\n     * @type {function}\n     * @example Settings.now = () => Date.now() + 3000 // pretend it is 3 seconds in the future\n     * @example Settings.now = () => 0 // always pretend it's Jan 1, 1970 at midnight in UTC time\n     */\n    static set now(n) {\n      now = n;\n    }\n\n    /**\n     * Set the default time zone to create DateTimes in. Does not affect existing instances.\n     * Use the value \"system\" to reset this value to the system's time zone.\n     * @type {string}\n     */\n    static set defaultZone(zone) {\n      defaultZone = zone;\n    }\n\n    /**\n     * Get the default time zone object currently used to create DateTimes. Does not affect existing instances.\n     * The default value is the system's time zone (the one set on the machine that runs this code).\n     * @type {Zone}\n     */\n    static get defaultZone() {\n      return normalizeZone(defaultZone, SystemZone.instance);\n    }\n\n    /**\n     * Get the default locale to create DateTimes with. Does not affect existing instances.\n     * @type {string}\n     */\n    static get defaultLocale() {\n      return defaultLocale;\n    }\n\n    /**\n     * Set the default locale to create DateTimes with. Does not affect existing instances.\n     * @type {string}\n     */\n    static set defaultLocale(locale) {\n      defaultLocale = locale;\n    }\n\n    /**\n     * Get the default numbering system to create DateTimes with. Does not affect existing instances.\n     * @type {string}\n     */\n    static get defaultNumberingSystem() {\n      return defaultNumberingSystem;\n    }\n\n    /**\n     * Set the default numbering system to create DateTimes with. Does not affect existing instances.\n     * @type {string}\n     */\n    static set defaultNumberingSystem(numberingSystem) {\n      defaultNumberingSystem = numberingSystem;\n    }\n\n    /**\n     * Get the default output calendar to create DateTimes with. Does not affect existing instances.\n     * @type {string}\n     */\n    static get defaultOutputCalendar() {\n      return defaultOutputCalendar;\n    }\n\n    /**\n     * Set the default output calendar to create DateTimes with. Does not affect existing instances.\n     * @type {string}\n     */\n    static set defaultOutputCalendar(outputCalendar) {\n      defaultOutputCalendar = outputCalendar;\n    }\n\n    /**\n     * @typedef {Object} WeekSettings\n     * @property {number} firstDay\n     * @property {number} minimalDays\n     * @property {number[]} weekend\n     */\n\n    /**\n     * @return {WeekSettings|null}\n     */\n    static get defaultWeekSettings() {\n      return defaultWeekSettings;\n    }\n\n    /**\n     * Allows overriding the default locale week settings, i.e. the start of the week, the weekend and\n     * how many days are required in the first week of a year.\n     * Does not affect existing instances.\n     *\n     * @param {WeekSettings|null} weekSettings\n     */\n    static set defaultWeekSettings(weekSettings) {\n      defaultWeekSettings = validateWeekSettings(weekSettings);\n    }\n\n    /**\n     * Get the cutoff year after which a string encoding a year as two digits is interpreted to occur in the current century.\n     * @type {number}\n     */\n    static get twoDigitCutoffYear() {\n      return twoDigitCutoffYear;\n    }\n\n    /**\n     * Set the cutoff year after which a string encoding a year as two digits is interpreted to occur in the current century.\n     * @type {number}\n     * @example Settings.twoDigitCutoffYear = 0 // cut-off year is 0, so all 'yy' are interpreted as current century\n     * @example Settings.twoDigitCutoffYear = 50 // '49' -> 1949; '50' -> 2050\n     * @example Settings.twoDigitCutoffYear = 1950 // interpreted as 50\n     * @example Settings.twoDigitCutoffYear = 2050 // ALSO interpreted as 50\n     */\n    static set twoDigitCutoffYear(cutoffYear) {\n      twoDigitCutoffYear = cutoffYear % 100;\n    }\n\n    /**\n     * Get whether Luxon will throw when it encounters invalid DateTimes, Durations, or Intervals\n     * @type {boolean}\n     */\n    static get throwOnInvalid() {\n      return throwOnInvalid;\n    }\n\n    /**\n     * Set whether Luxon will throw when it encounters invalid DateTimes, Durations, or Intervals\n     * @type {boolean}\n     */\n    static set throwOnInvalid(t) {\n      throwOnInvalid = t;\n    }\n\n    /**\n     * Reset Luxon's global caches. Should only be necessary in testing scenarios.\n     * @return {void}\n     */\n    static resetCaches() {\n      Locale.resetCache();\n      IANAZone.resetCache();\n    }\n  }\n\n  class Invalid {\n    constructor(reason, explanation) {\n      this.reason = reason;\n      this.explanation = explanation;\n    }\n\n    toMessage() {\n      if (this.explanation) {\n        return `${this.reason}: ${this.explanation}`;\n      } else {\n        return this.reason;\n      }\n    }\n  }\n\n  const nonLeapLadder = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334],\n    leapLadder = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335];\n\n  function unitOutOfRange(unit, value) {\n    return new Invalid(\n      \"unit out of range\",\n      `you specified ${value} (of type ${typeof value}) as a ${unit}, which is invalid`\n    );\n  }\n\n  function dayOfWeek(year, month, day) {\n    const d = new Date(Date.UTC(year, month - 1, day));\n\n    if (year < 100 && year >= 0) {\n      d.setUTCFullYear(d.getUTCFullYear() - 1900);\n    }\n\n    const js = d.getUTCDay();\n\n    return js === 0 ? 7 : js;\n  }\n\n  function computeOrdinal(year, month, day) {\n    return day + (isLeapYear(year) ? leapLadder : nonLeapLadder)[month - 1];\n  }\n\n  function uncomputeOrdinal(year, ordinal) {\n    const table = isLeapYear(year) ? leapLadder : nonLeapLadder,\n      month0 = table.findIndex((i) => i < ordinal),\n      day = ordinal - table[month0];\n    return { month: month0 + 1, day };\n  }\n\n  function isoWeekdayToLocal(isoWeekday, startOfWeek) {\n    return ((isoWeekday - startOfWeek + 7) % 7) + 1;\n  }\n\n  /**\n   * @private\n   */\n\n  function gregorianToWeek(gregObj, minDaysInFirstWeek = 4, startOfWeek = 1) {\n    const { year, month, day } = gregObj,\n      ordinal = computeOrdinal(year, month, day),\n      weekday = isoWeekdayToLocal(dayOfWeek(year, month, day), startOfWeek);\n\n    let weekNumber = Math.floor((ordinal - weekday + 14 - minDaysInFirstWeek) / 7),\n      weekYear;\n\n    if (weekNumber < 1) {\n      weekYear = year - 1;\n      weekNumber = weeksInWeekYear(weekYear, minDaysInFirstWeek, startOfWeek);\n    } else if (weekNumber > weeksInWeekYear(year, minDaysInFirstWeek, startOfWeek)) {\n      weekYear = year + 1;\n      weekNumber = 1;\n    } else {\n      weekYear = year;\n    }\n\n    return { weekYear, weekNumber, weekday, ...timeObject(gregObj) };\n  }\n\n  function weekToGregorian(weekData, minDaysInFirstWeek = 4, startOfWeek = 1) {\n    const { weekYear, weekNumber, weekday } = weekData,\n      weekdayOfJan4 = isoWeekdayToLocal(dayOfWeek(weekYear, 1, minDaysInFirstWeek), startOfWeek),\n      yearInDays = daysInYear(weekYear);\n\n    let ordinal = weekNumber * 7 + weekday - weekdayOfJan4 - 7 + minDaysInFirstWeek,\n      year;\n\n    if (ordinal < 1) {\n      year = weekYear - 1;\n      ordinal += daysInYear(year);\n    } else if (ordinal > yearInDays) {\n      year = weekYear + 1;\n      ordinal -= daysInYear(weekYear);\n    } else {\n      year = weekYear;\n    }\n\n    const { month, day } = uncomputeOrdinal(year, ordinal);\n    return { year, month, day, ...timeObject(weekData) };\n  }\n\n  function gregorianToOrdinal(gregData) {\n    const { year, month, day } = gregData;\n    const ordinal = computeOrdinal(year, month, day);\n    return { year, ordinal, ...timeObject(gregData) };\n  }\n\n  function ordinalToGregorian(ordinalData) {\n    const { year, ordinal } = ordinalData;\n    const { month, day } = uncomputeOrdinal(year, ordinal);\n    return { year, month, day, ...timeObject(ordinalData) };\n  }\n\n  /**\n   * Check if local week units like localWeekday are used in obj.\n   * If so, validates that they are not mixed with ISO week units and then copies them to the normal week unit properties.\n   * Modifies obj in-place!\n   * @param obj the object values\n   */\n  function usesLocalWeekValues(obj, loc) {\n    const hasLocaleWeekData =\n      !isUndefined(obj.localWeekday) ||\n      !isUndefined(obj.localWeekNumber) ||\n      !isUndefined(obj.localWeekYear);\n    if (hasLocaleWeekData) {\n      const hasIsoWeekData =\n        !isUndefined(obj.weekday) || !isUndefined(obj.weekNumber) || !isUndefined(obj.weekYear);\n\n      if (hasIsoWeekData) {\n        throw new ConflictingSpecificationError(\n          \"Cannot mix locale-based week fields with ISO-based week fields\"\n        );\n      }\n      if (!isUndefined(obj.localWeekday)) obj.weekday = obj.localWeekday;\n      if (!isUndefined(obj.localWeekNumber)) obj.weekNumber = obj.localWeekNumber;\n      if (!isUndefined(obj.localWeekYear)) obj.weekYear = obj.localWeekYear;\n      delete obj.localWeekday;\n      delete obj.localWeekNumber;\n      delete obj.localWeekYear;\n      return {\n        minDaysInFirstWeek: loc.getMinDaysInFirstWeek(),\n        startOfWeek: loc.getStartOfWeek(),\n      };\n    } else {\n      return { minDaysInFirstWeek: 4, startOfWeek: 1 };\n    }\n  }\n\n  function hasInvalidWeekData(obj, minDaysInFirstWeek = 4, startOfWeek = 1) {\n    const validYear = isInteger(obj.weekYear),\n      validWeek = integerBetween(\n        obj.weekNumber,\n        1,\n        weeksInWeekYear(obj.weekYear, minDaysInFirstWeek, startOfWeek)\n      ),\n      validWeekday = integerBetween(obj.weekday, 1, 7);\n\n    if (!validYear) {\n      return unitOutOfRange(\"weekYear\", obj.weekYear);\n    } else if (!validWeek) {\n      return unitOutOfRange(\"week\", obj.weekNumber);\n    } else if (!validWeekday) {\n      return unitOutOfRange(\"weekday\", obj.weekday);\n    } else return false;\n  }\n\n  function hasInvalidOrdinalData(obj) {\n    const validYear = isInteger(obj.year),\n      validOrdinal = integerBetween(obj.ordinal, 1, daysInYear(obj.year));\n\n    if (!validYear) {\n      return unitOutOfRange(\"year\", obj.year);\n    } else if (!validOrdinal) {\n      return unitOutOfRange(\"ordinal\", obj.ordinal);\n    } else return false;\n  }\n\n  function hasInvalidGregorianData(obj) {\n    const validYear = isInteger(obj.year),\n      validMonth = integerBetween(obj.month, 1, 12),\n      validDay = integerBetween(obj.day, 1, daysInMonth(obj.year, obj.month));\n\n    if (!validYear) {\n      return unitOutOfRange(\"year\", obj.year);\n    } else if (!validMonth) {\n      return unitOutOfRange(\"month\", obj.month);\n    } else if (!validDay) {\n      return unitOutOfRange(\"day\", obj.day);\n    } else return false;\n  }\n\n  function hasInvalidTimeData(obj) {\n    const { hour, minute, second, millisecond } = obj;\n    const validHour =\n        integerBetween(hour, 0, 23) ||\n        (hour === 24 && minute === 0 && second === 0 && millisecond === 0),\n      validMinute = integerBetween(minute, 0, 59),\n      validSecond = integerBetween(second, 0, 59),\n      validMillisecond = integerBetween(millisecond, 0, 999);\n\n    if (!validHour) {\n      return unitOutOfRange(\"hour\", hour);\n    } else if (!validMinute) {\n      return unitOutOfRange(\"minute\", minute);\n    } else if (!validSecond) {\n      return unitOutOfRange(\"second\", second);\n    } else if (!validMillisecond) {\n      return unitOutOfRange(\"millisecond\", millisecond);\n    } else return false;\n  }\n\n  /*\n    This is just a junk drawer, containing anything used across multiple classes.\n    Because Luxon is small(ish), this should stay small and we won't worry about splitting\n    it up into, say, parsingUtil.js and basicUtil.js and so on. But they are divided up by feature area.\n  */\n\n  /**\n   * @private\n   */\n\n  // TYPES\n\n  function isUndefined(o) {\n    return typeof o === \"undefined\";\n  }\n\n  function isNumber(o) {\n    return typeof o === \"number\";\n  }\n\n  function isInteger(o) {\n    return typeof o === \"number\" && o % 1 === 0;\n  }\n\n  function isString(o) {\n    return typeof o === \"string\";\n  }\n\n  function isDate(o) {\n    return Object.prototype.toString.call(o) === \"[object Date]\";\n  }\n\n  // CAPABILITIES\n\n  function hasRelative() {\n    try {\n      return typeof Intl !== \"undefined\" && !!Intl.RelativeTimeFormat;\n    } catch (e) {\n      return false;\n    }\n  }\n\n  function hasLocaleWeekInfo() {\n    try {\n      return (\n        typeof Intl !== \"undefined\" &&\n        !!Intl.Locale &&\n        (\"weekInfo\" in Intl.Locale.prototype || \"getWeekInfo\" in Intl.Locale.prototype)\n      );\n    } catch (e) {\n      return false;\n    }\n  }\n\n  // OBJECTS AND ARRAYS\n\n  function maybeArray(thing) {\n    return Array.isArray(thing) ? thing : [thing];\n  }\n\n  function bestBy(arr, by, compare) {\n    if (arr.length === 0) {\n      return undefined;\n    }\n    return arr.reduce((best, next) => {\n      const pair = [by(next), next];\n      if (!best) {\n        return pair;\n      } else if (compare(best[0], pair[0]) === best[0]) {\n        return best;\n      } else {\n        return pair;\n      }\n    }, null)[1];\n  }\n\n  function pick(obj, keys) {\n    return keys.reduce((a, k) => {\n      a[k] = obj[k];\n      return a;\n    }, {});\n  }\n\n  function hasOwnProperty(obj, prop) {\n    return Object.prototype.hasOwnProperty.call(obj, prop);\n  }\n\n  function validateWeekSettings(settings) {\n    if (settings == null) {\n      return null;\n    } else if (typeof settings !== \"object\") {\n      throw new InvalidArgumentError(\"Week settings must be an object\");\n    } else {\n      if (\n        !integerBetween(settings.firstDay, 1, 7) ||\n        !integerBetween(settings.minimalDays, 1, 7) ||\n        !Array.isArray(settings.weekend) ||\n        settings.weekend.some((v) => !integerBetween(v, 1, 7))\n      ) {\n        throw new InvalidArgumentError(\"Invalid week settings\");\n      }\n      return {\n        firstDay: settings.firstDay,\n        minimalDays: settings.minimalDays,\n        weekend: Array.from(settings.weekend),\n      };\n    }\n  }\n\n  // NUMBERS AND STRINGS\n\n  function integerBetween(thing, bottom, top) {\n    return isInteger(thing) && thing >= bottom && thing <= top;\n  }\n\n  // x % n but takes the sign of n instead of x\n  function floorMod(x, n) {\n    return x - n * Math.floor(x / n);\n  }\n\n  function padStart(input, n = 2) {\n    const isNeg = input < 0;\n    let padded;\n    if (isNeg) {\n      padded = \"-\" + (\"\" + -input).padStart(n, \"0\");\n    } else {\n      padded = (\"\" + input).padStart(n, \"0\");\n    }\n    return padded;\n  }\n\n  function parseInteger(string) {\n    if (isUndefined(string) || string === null || string === \"\") {\n      return undefined;\n    } else {\n      return parseInt(string, 10);\n    }\n  }\n\n  function parseFloating(string) {\n    if (isUndefined(string) || string === null || string === \"\") {\n      return undefined;\n    } else {\n      return parseFloat(string);\n    }\n  }\n\n  function parseMillis(fraction) {\n    // Return undefined (instead of 0) in these cases, where fraction is not set\n    if (isUndefined(fraction) || fraction === null || fraction === \"\") {\n      return undefined;\n    } else {\n      const f = parseFloat(\"0.\" + fraction) * 1000;\n      return Math.floor(f);\n    }\n  }\n\n  function roundTo(number, digits, towardZero = false) {\n    const factor = 10 ** digits,\n      rounder = towardZero ? Math.trunc : Math.round;\n    return rounder(number * factor) / factor;\n  }\n\n  // DATE BASICS\n\n  function isLeapYear(year) {\n    return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);\n  }\n\n  function daysInYear(year) {\n    return isLeapYear(year) ? 366 : 365;\n  }\n\n  function daysInMonth(year, month) {\n    const modMonth = floorMod(month - 1, 12) + 1,\n      modYear = year + (month - modMonth) / 12;\n\n    if (modMonth === 2) {\n      return isLeapYear(modYear) ? 29 : 28;\n    } else {\n      return [31, null, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][modMonth - 1];\n    }\n  }\n\n  // convert a calendar object to a local timestamp (epoch, but with the offset baked in)\n  function objToLocalTS(obj) {\n    let d = Date.UTC(\n      obj.year,\n      obj.month - 1,\n      obj.day,\n      obj.hour,\n      obj.minute,\n      obj.second,\n      obj.millisecond\n    );\n\n    // for legacy reasons, years between 0 and 99 are interpreted as 19XX; revert that\n    if (obj.year < 100 && obj.year >= 0) {\n      d = new Date(d);\n      // set the month and day again, this is necessary because year 2000 is a leap year, but year 100 is not\n      // so if obj.year is in 99, but obj.day makes it roll over into year 100,\n      // the calculations done by Date.UTC are using year 2000 - which is incorrect\n      d.setUTCFullYear(obj.year, obj.month - 1, obj.day);\n    }\n    return +d;\n  }\n\n  // adapted from moment.js: https://github.com/moment/moment/blob/000ac1800e620f770f4eb31b5ae908f6167b0ab2/src/lib/units/week-calendar-utils.js\n  function firstWeekOffset(year, minDaysInFirstWeek, startOfWeek) {\n    const fwdlw = isoWeekdayToLocal(dayOfWeek(year, 1, minDaysInFirstWeek), startOfWeek);\n    return -fwdlw + minDaysInFirstWeek - 1;\n  }\n\n  function weeksInWeekYear(weekYear, minDaysInFirstWeek = 4, startOfWeek = 1) {\n    const weekOffset = firstWeekOffset(weekYear, minDaysInFirstWeek, startOfWeek);\n    const weekOffsetNext = firstWeekOffset(weekYear + 1, minDaysInFirstWeek, startOfWeek);\n    return (daysInYear(weekYear) - weekOffset + weekOffsetNext) / 7;\n  }\n\n  function untruncateYear(year) {\n    if (year > 99) {\n      return year;\n    } else return year > Settings.twoDigitCutoffYear ? 1900 + year : 2000 + year;\n  }\n\n  // PARSING\n\n  function parseZoneInfo(ts, offsetFormat, locale, timeZone = null) {\n    const date = new Date(ts),\n      intlOpts = {\n        hourCycle: \"h23\",\n        year: \"numeric\",\n        month: \"2-digit\",\n        day: \"2-digit\",\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n      };\n\n    if (timeZone) {\n      intlOpts.timeZone = timeZone;\n    }\n\n    const modified = { timeZoneName: offsetFormat, ...intlOpts };\n\n    const parsed = new Intl.DateTimeFormat(locale, modified)\n      .formatToParts(date)\n      .find((m) => m.type.toLowerCase() === \"timezonename\");\n    return parsed ? parsed.value : null;\n  }\n\n  // signedOffset('-5', '30') -> -330\n  function signedOffset(offHourStr, offMinuteStr) {\n    let offHour = parseInt(offHourStr, 10);\n\n    // don't || this because we want to preserve -0\n    if (Number.isNaN(offHour)) {\n      offHour = 0;\n    }\n\n    const offMin = parseInt(offMinuteStr, 10) || 0,\n      offMinSigned = offHour < 0 || Object.is(offHour, -0) ? -offMin : offMin;\n    return offHour * 60 + offMinSigned;\n  }\n\n  // COERCION\n\n  function asNumber(value) {\n    const numericValue = Number(value);\n    if (typeof value === \"boolean\" || value === \"\" || Number.isNaN(numericValue))\n      throw new InvalidArgumentError(`Invalid unit value ${value}`);\n    return numericValue;\n  }\n\n  function normalizeObject(obj, normalizer) {\n    const normalized = {};\n    for (const u in obj) {\n      if (hasOwnProperty(obj, u)) {\n        const v = obj[u];\n        if (v === undefined || v === null) continue;\n        normalized[normalizer(u)] = asNumber(v);\n      }\n    }\n    return normalized;\n  }\n\n  function formatOffset(offset, format) {\n    const hours = Math.trunc(Math.abs(offset / 60)),\n      minutes = Math.trunc(Math.abs(offset % 60)),\n      sign = offset >= 0 ? \"+\" : \"-\";\n\n    switch (format) {\n      case \"short\":\n        return `${sign}${padStart(hours, 2)}:${padStart(minutes, 2)}`;\n      case \"narrow\":\n        return `${sign}${hours}${minutes > 0 ? `:${minutes}` : \"\"}`;\n      case \"techie\":\n        return `${sign}${padStart(hours, 2)}${padStart(minutes, 2)}`;\n      default:\n        throw new RangeError(`Value format ${format} is out of range for property format`);\n    }\n  }\n\n  function timeObject(obj) {\n    return pick(obj, [\"hour\", \"minute\", \"second\", \"millisecond\"]);\n  }\n\n  /**\n   * @private\n   */\n\n  const monthsLong = [\n    \"January\",\n    \"February\",\n    \"March\",\n    \"April\",\n    \"May\",\n    \"June\",\n    \"July\",\n    \"August\",\n    \"September\",\n    \"October\",\n    \"November\",\n    \"December\",\n  ];\n\n  const monthsShort = [\n    \"Jan\",\n    \"Feb\",\n    \"Mar\",\n    \"Apr\",\n    \"May\",\n    \"Jun\",\n    \"Jul\",\n    \"Aug\",\n    \"Sep\",\n    \"Oct\",\n    \"Nov\",\n    \"Dec\",\n  ];\n\n  const monthsNarrow = [\"J\", \"F\", \"M\", \"A\", \"M\", \"J\", \"J\", \"A\", \"S\", \"O\", \"N\", \"D\"];\n\n  function months(length) {\n    switch (length) {\n      case \"narrow\":\n        return [...monthsNarrow];\n      case \"short\":\n        return [...monthsShort];\n      case \"long\":\n        return [...monthsLong];\n      case \"numeric\":\n        return [\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"10\", \"11\", \"12\"];\n      case \"2-digit\":\n        return [\"01\", \"02\", \"03\", \"04\", \"05\", \"06\", \"07\", \"08\", \"09\", \"10\", \"11\", \"12\"];\n      default:\n        return null;\n    }\n  }\n\n  const weekdaysLong = [\n    \"Monday\",\n    \"Tuesday\",\n    \"Wednesday\",\n    \"Thursday\",\n    \"Friday\",\n    \"Saturday\",\n    \"Sunday\",\n  ];\n\n  const weekdaysShort = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"];\n\n  const weekdaysNarrow = [\"M\", \"T\", \"W\", \"T\", \"F\", \"S\", \"S\"];\n\n  function weekdays(length) {\n    switch (length) {\n      case \"narrow\":\n        return [...weekdaysNarrow];\n      case \"short\":\n        return [...weekdaysShort];\n      case \"long\":\n        return [...weekdaysLong];\n      case \"numeric\":\n        return [\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\"];\n      default:\n        return null;\n    }\n  }\n\n  const meridiems = [\"AM\", \"PM\"];\n\n  const erasLong = [\"Before Christ\", \"Anno Domini\"];\n\n  const erasShort = [\"BC\", \"AD\"];\n\n  const erasNarrow = [\"B\", \"A\"];\n\n  function eras(length) {\n    switch (length) {\n      case \"narrow\":\n        return [...erasNarrow];\n      case \"short\":\n        return [...erasShort];\n      case \"long\":\n        return [...erasLong];\n      default:\n        return null;\n    }\n  }\n\n  function meridiemForDateTime(dt) {\n    return meridiems[dt.hour < 12 ? 0 : 1];\n  }\n\n  function weekdayForDateTime(dt, length) {\n    return weekdays(length)[dt.weekday - 1];\n  }\n\n  function monthForDateTime(dt, length) {\n    return months(length)[dt.month - 1];\n  }\n\n  function eraForDateTime(dt, length) {\n    return eras(length)[dt.year < 0 ? 0 : 1];\n  }\n\n  function formatRelativeTime(unit, count, numeric = \"always\", narrow = false) {\n    const units = {\n      years: [\"year\", \"yr.\"],\n      quarters: [\"quarter\", \"qtr.\"],\n      months: [\"month\", \"mo.\"],\n      weeks: [\"week\", \"wk.\"],\n      days: [\"day\", \"day\", \"days\"],\n      hours: [\"hour\", \"hr.\"],\n      minutes: [\"minute\", \"min.\"],\n      seconds: [\"second\", \"sec.\"],\n    };\n\n    const lastable = [\"hours\", \"minutes\", \"seconds\"].indexOf(unit) === -1;\n\n    if (numeric === \"auto\" && lastable) {\n      const isDay = unit === \"days\";\n      switch (count) {\n        case 1:\n          return isDay ? \"tomorrow\" : `next ${units[unit][0]}`;\n        case -1:\n          return isDay ? \"yesterday\" : `last ${units[unit][0]}`;\n        case 0:\n          return isDay ? \"today\" : `this ${units[unit][0]}`;\n      }\n    }\n\n    const isInPast = Object.is(count, -0) || count < 0,\n      fmtValue = Math.abs(count),\n      singular = fmtValue === 1,\n      lilUnits = units[unit],\n      fmtUnit = narrow\n        ? singular\n          ? lilUnits[1]\n          : lilUnits[2] || lilUnits[1]\n        : singular\n        ? units[unit][0]\n        : unit;\n    return isInPast ? `${fmtValue} ${fmtUnit} ago` : `in ${fmtValue} ${fmtUnit}`;\n  }\n\n  function stringifyTokens(splits, tokenToString) {\n    let s = \"\";\n    for (const token of splits) {\n      if (token.literal) {\n        s += token.val;\n      } else {\n        s += tokenToString(token.val);\n      }\n    }\n    return s;\n  }\n\n  const macroTokenToFormatOpts = {\n    D: DATE_SHORT,\n    DD: DATE_MED,\n    DDD: DATE_FULL,\n    DDDD: DATE_HUGE,\n    t: TIME_SIMPLE,\n    tt: TIME_WITH_SECONDS,\n    ttt: TIME_WITH_SHORT_OFFSET,\n    tttt: TIME_WITH_LONG_OFFSET,\n    T: TIME_24_SIMPLE,\n    TT: TIME_24_WITH_SECONDS,\n    TTT: TIME_24_WITH_SHORT_OFFSET,\n    TTTT: TIME_24_WITH_LONG_OFFSET,\n    f: DATETIME_SHORT,\n    ff: DATETIME_MED,\n    fff: DATETIME_FULL,\n    ffff: DATETIME_HUGE,\n    F: DATETIME_SHORT_WITH_SECONDS,\n    FF: DATETIME_MED_WITH_SECONDS,\n    FFF: DATETIME_FULL_WITH_SECONDS,\n    FFFF: DATETIME_HUGE_WITH_SECONDS,\n  };\n\n  /**\n   * @private\n   */\n\n  class Formatter {\n    static create(locale, opts = {}) {\n      return new Formatter(locale, opts);\n    }\n\n    static parseFormat(fmt) {\n      // white-space is always considered a literal in user-provided formats\n      // the \" \" token has a special meaning (see unitForToken)\n\n      let current = null,\n        currentFull = \"\",\n        bracketed = false;\n      const splits = [];\n      for (let i = 0; i < fmt.length; i++) {\n        const c = fmt.charAt(i);\n        if (c === \"'\") {\n          if (currentFull.length > 0) {\n            splits.push({ literal: bracketed || /^\\s+$/.test(currentFull), val: currentFull });\n          }\n          current = null;\n          currentFull = \"\";\n          bracketed = !bracketed;\n        } else if (bracketed) {\n          currentFull += c;\n        } else if (c === current) {\n          currentFull += c;\n        } else {\n          if (currentFull.length > 0) {\n            splits.push({ literal: /^\\s+$/.test(currentFull), val: currentFull });\n          }\n          currentFull = c;\n          current = c;\n        }\n      }\n\n      if (currentFull.length > 0) {\n        splits.push({ literal: bracketed || /^\\s+$/.test(currentFull), val: currentFull });\n      }\n\n      return splits;\n    }\n\n    static macroTokenToFormatOpts(token) {\n      return macroTokenToFormatOpts[token];\n    }\n\n    constructor(locale, formatOpts) {\n      this.opts = formatOpts;\n      this.loc = locale;\n      this.systemLoc = null;\n    }\n\n    formatWithSystemDefault(dt, opts) {\n      if (this.systemLoc === null) {\n        this.systemLoc = this.loc.redefaultToSystem();\n      }\n      const df = this.systemLoc.dtFormatter(dt, { ...this.opts, ...opts });\n      return df.format();\n    }\n\n    dtFormatter(dt, opts = {}) {\n      return this.loc.dtFormatter(dt, { ...this.opts, ...opts });\n    }\n\n    formatDateTime(dt, opts) {\n      return this.dtFormatter(dt, opts).format();\n    }\n\n    formatDateTimeParts(dt, opts) {\n      return this.dtFormatter(dt, opts).formatToParts();\n    }\n\n    formatInterval(interval, opts) {\n      const df = this.dtFormatter(interval.start, opts);\n      return df.dtf.formatRange(interval.start.toJSDate(), interval.end.toJSDate());\n    }\n\n    resolvedOptions(dt, opts) {\n      return this.dtFormatter(dt, opts).resolvedOptions();\n    }\n\n    num(n, p = 0) {\n      // we get some perf out of doing this here, annoyingly\n      if (this.opts.forceSimple) {\n        return padStart(n, p);\n      }\n\n      const opts = { ...this.opts };\n\n      if (p > 0) {\n        opts.padTo = p;\n      }\n\n      return this.loc.numberFormatter(opts).format(n);\n    }\n\n    formatDateTimeFromString(dt, fmt) {\n      const knownEnglish = this.loc.listingMode() === \"en\",\n        useDateTimeFormatter = this.loc.outputCalendar && this.loc.outputCalendar !== \"gregory\",\n        string = (opts, extract) => this.loc.extract(dt, opts, extract),\n        formatOffset = (opts) => {\n          if (dt.isOffsetFixed && dt.offset === 0 && opts.allowZ) {\n            return \"Z\";\n          }\n\n          return dt.isValid ? dt.zone.formatOffset(dt.ts, opts.format) : \"\";\n        },\n        meridiem = () =>\n          knownEnglish\n            ? meridiemForDateTime(dt)\n            : string({ hour: \"numeric\", hourCycle: \"h12\" }, \"dayperiod\"),\n        month = (length, standalone) =>\n          knownEnglish\n            ? monthForDateTime(dt, length)\n            : string(standalone ? { month: length } : { month: length, day: \"numeric\" }, \"month\"),\n        weekday = (length, standalone) =>\n          knownEnglish\n            ? weekdayForDateTime(dt, length)\n            : string(\n                standalone ? { weekday: length } : { weekday: length, month: \"long\", day: \"numeric\" },\n                \"weekday\"\n              ),\n        maybeMacro = (token) => {\n          const formatOpts = Formatter.macroTokenToFormatOpts(token);\n          if (formatOpts) {\n            return this.formatWithSystemDefault(dt, formatOpts);\n          } else {\n            return token;\n          }\n        },\n        era = (length) =>\n          knownEnglish ? eraForDateTime(dt, length) : string({ era: length }, \"era\"),\n        tokenToString = (token) => {\n          // Where possible: https://cldr.unicode.org/translation/date-time/date-time-symbols\n          switch (token) {\n            // ms\n            case \"S\":\n              return this.num(dt.millisecond);\n            case \"u\":\n            // falls through\n            case \"SSS\":\n              return this.num(dt.millisecond, 3);\n            // seconds\n            case \"s\":\n              return this.num(dt.second);\n            case \"ss\":\n              return this.num(dt.second, 2);\n            // fractional seconds\n            case \"uu\":\n              return this.num(Math.floor(dt.millisecond / 10), 2);\n            case \"uuu\":\n              return this.num(Math.floor(dt.millisecond / 100));\n            // minutes\n            case \"m\":\n              return this.num(dt.minute);\n            case \"mm\":\n              return this.num(dt.minute, 2);\n            // hours\n            case \"h\":\n              return this.num(dt.hour % 12 === 0 ? 12 : dt.hour % 12);\n            case \"hh\":\n              return this.num(dt.hour % 12 === 0 ? 12 : dt.hour % 12, 2);\n            case \"H\":\n              return this.num(dt.hour);\n            case \"HH\":\n              return this.num(dt.hour, 2);\n            // offset\n            case \"Z\":\n              // like +6\n              return formatOffset({ format: \"narrow\", allowZ: this.opts.allowZ });\n            case \"ZZ\":\n              // like +06:00\n              return formatOffset({ format: \"short\", allowZ: this.opts.allowZ });\n            case \"ZZZ\":\n              // like +0600\n              return formatOffset({ format: \"techie\", allowZ: this.opts.allowZ });\n            case \"ZZZZ\":\n              // like EST\n              return dt.zone.offsetName(dt.ts, { format: \"short\", locale: this.loc.locale });\n            case \"ZZZZZ\":\n              // like Eastern Standard Time\n              return dt.zone.offsetName(dt.ts, { format: \"long\", locale: this.loc.locale });\n            // zone\n            case \"z\":\n              // like America/New_York\n              return dt.zoneName;\n            // meridiems\n            case \"a\":\n              return meridiem();\n            // dates\n            case \"d\":\n              return useDateTimeFormatter ? string({ day: \"numeric\" }, \"day\") : this.num(dt.day);\n            case \"dd\":\n              return useDateTimeFormatter ? string({ day: \"2-digit\" }, \"day\") : this.num(dt.day, 2);\n            // weekdays - standalone\n            case \"c\":\n              // like 1\n              return this.num(dt.weekday);\n            case \"ccc\":\n              // like 'Tues'\n              return weekday(\"short\", true);\n            case \"cccc\":\n              // like 'Tuesday'\n              return weekday(\"long\", true);\n            case \"ccccc\":\n              // like 'T'\n              return weekday(\"narrow\", true);\n            // weekdays - format\n            case \"E\":\n              // like 1\n              return this.num(dt.weekday);\n            case \"EEE\":\n              // like 'Tues'\n              return weekday(\"short\", false);\n            case \"EEEE\":\n              // like 'Tuesday'\n              return weekday(\"long\", false);\n            case \"EEEEE\":\n              // like 'T'\n              return weekday(\"narrow\", false);\n            // months - standalone\n            case \"L\":\n              // like 1\n              return useDateTimeFormatter\n                ? string({ month: \"numeric\", day: \"numeric\" }, \"month\")\n                : this.num(dt.month);\n            case \"LL\":\n              // like 01, doesn't seem to work\n              return useDateTimeFormatter\n                ? string({ month: \"2-digit\", day: \"numeric\" }, \"month\")\n                : this.num(dt.month, 2);\n            case \"LLL\":\n              // like Jan\n              return month(\"short\", true);\n            case \"LLLL\":\n              // like January\n              return month(\"long\", true);\n            case \"LLLLL\":\n              // like J\n              return month(\"narrow\", true);\n            // months - format\n            case \"M\":\n              // like 1\n              return useDateTimeFormatter\n                ? string({ month: \"numeric\" }, \"month\")\n                : this.num(dt.month);\n            case \"MM\":\n              // like 01\n              return useDateTimeFormatter\n                ? string({ month: \"2-digit\" }, \"month\")\n                : this.num(dt.month, 2);\n            case \"MMM\":\n              // like Jan\n              return month(\"short\", false);\n            case \"MMMM\":\n              // like January\n              return month(\"long\", false);\n            case \"MMMMM\":\n              // like J\n              return month(\"narrow\", false);\n            // years\n            case \"y\":\n              // like 2014\n              return useDateTimeFormatter ? string({ year: \"numeric\" }, \"year\") : this.num(dt.year);\n            case \"yy\":\n              // like 14\n              return useDateTimeFormatter\n                ? string({ year: \"2-digit\" }, \"year\")\n                : this.num(dt.year.toString().slice(-2), 2);\n            case \"yyyy\":\n              // like 0012\n              return useDateTimeFormatter\n                ? string({ year: \"numeric\" }, \"year\")\n                : this.num(dt.year, 4);\n            case \"yyyyyy\":\n              // like 000012\n              return useDateTimeFormatter\n                ? string({ year: \"numeric\" }, \"year\")\n                : this.num(dt.year, 6);\n            // eras\n            case \"G\":\n              // like AD\n              return era(\"short\");\n            case \"GG\":\n              // like Anno Domini\n              return era(\"long\");\n            case \"GGGGG\":\n              return era(\"narrow\");\n            case \"kk\":\n              return this.num(dt.weekYear.toString().slice(-2), 2);\n            case \"kkkk\":\n              return this.num(dt.weekYear, 4);\n            case \"W\":\n              return this.num(dt.weekNumber);\n            case \"WW\":\n              return this.num(dt.weekNumber, 2);\n            case \"n\":\n              return this.num(dt.localWeekNumber);\n            case \"nn\":\n              return this.num(dt.localWeekNumber, 2);\n            case \"ii\":\n              return this.num(dt.localWeekYear.toString().slice(-2), 2);\n            case \"iiii\":\n              return this.num(dt.localWeekYear, 4);\n            case \"o\":\n              return this.num(dt.ordinal);\n            case \"ooo\":\n              return this.num(dt.ordinal, 3);\n            case \"q\":\n              // like 1\n              return this.num(dt.quarter);\n            case \"qq\":\n              // like 01\n              return this.num(dt.quarter, 2);\n            case \"X\":\n              return this.num(Math.floor(dt.ts / 1000));\n            case \"x\":\n              return this.num(dt.ts);\n            default:\n              return maybeMacro(token);\n          }\n        };\n\n      return stringifyTokens(Formatter.parseFormat(fmt), tokenToString);\n    }\n\n    formatDurationFromString(dur, fmt) {\n      const tokenToField = (token) => {\n          switch (token[0]) {\n            case \"S\":\n              return \"millisecond\";\n            case \"s\":\n              return \"second\";\n            case \"m\":\n              return \"minute\";\n            case \"h\":\n              return \"hour\";\n            case \"d\":\n              return \"day\";\n            case \"w\":\n              return \"week\";\n            case \"M\":\n              return \"month\";\n            case \"y\":\n              return \"year\";\n            default:\n              return null;\n          }\n        },\n        tokenToString = (lildur) => (token) => {\n          const mapped = tokenToField(token);\n          if (mapped) {\n            return this.num(lildur.get(mapped), token.length);\n          } else {\n            return token;\n          }\n        },\n        tokens = Formatter.parseFormat(fmt),\n        realTokens = tokens.reduce(\n          (found, { literal, val }) => (literal ? found : found.concat(val)),\n          []\n        ),\n        collapsed = dur.shiftTo(...realTokens.map(tokenToField).filter((t) => t));\n      return stringifyTokens(tokens, tokenToString(collapsed));\n    }\n  }\n\n  /*\n   * This file handles parsing for well-specified formats. Here's how it works:\n   * Two things go into parsing: a regex to match with and an extractor to take apart the groups in the match.\n   * An extractor is just a function that takes a regex match array and returns a { year: ..., month: ... } object\n   * parse() does the work of executing the regex and applying the extractor. It takes multiple regex/extractor pairs to try in sequence.\n   * Extractors can take a \"cursor\" representing the offset in the match to look at. This makes it easy to combine extractors.\n   * combineExtractors() does the work of combining them, keeping track of the cursor through multiple extractions.\n   * Some extractions are super dumb and simpleParse and fromStrings help DRY them.\n   */\n\n  const ianaRegex = /[A-Za-z_+-]{1,256}(?::?\\/[A-Za-z0-9_+-]{1,256}(?:\\/[A-Za-z0-9_+-]{1,256})?)?/;\n\n  function combineRegexes(...regexes) {\n    const full = regexes.reduce((f, r) => f + r.source, \"\");\n    return RegExp(`^${full}$`);\n  }\n\n  function combineExtractors(...extractors) {\n    return (m) =>\n      extractors\n        .reduce(\n          ([mergedVals, mergedZone, cursor], ex) => {\n            const [val, zone, next] = ex(m, cursor);\n            return [{ ...mergedVals, ...val }, zone || mergedZone, next];\n          },\n          [{}, null, 1]\n        )\n        .slice(0, 2);\n  }\n\n  function parse(s, ...patterns) {\n    if (s == null) {\n      return [null, null];\n    }\n\n    for (const [regex, extractor] of patterns) {\n      const m = regex.exec(s);\n      if (m) {\n        return extractor(m);\n      }\n    }\n    return [null, null];\n  }\n\n  function simpleParse(...keys) {\n    return (match, cursor) => {\n      const ret = {};\n      let i;\n\n      for (i = 0; i < keys.length; i++) {\n        ret[keys[i]] = parseInteger(match[cursor + i]);\n      }\n      return [ret, null, cursor + i];\n    };\n  }\n\n  // ISO and SQL parsing\n  const offsetRegex = /(?:(Z)|([+-]\\d\\d)(?::?(\\d\\d))?)/;\n  const isoExtendedZone = `(?:${offsetRegex.source}?(?:\\\\[(${ianaRegex.source})\\\\])?)?`;\n  const isoTimeBaseRegex = /(\\d\\d)(?::?(\\d\\d)(?::?(\\d\\d)(?:[.,](\\d{1,30}))?)?)?/;\n  const isoTimeRegex = RegExp(`${isoTimeBaseRegex.source}${isoExtendedZone}`);\n  const isoTimeExtensionRegex = RegExp(`(?:T${isoTimeRegex.source})?`);\n  const isoYmdRegex = /([+-]\\d{6}|\\d{4})(?:-?(\\d\\d)(?:-?(\\d\\d))?)?/;\n  const isoWeekRegex = /(\\d{4})-?W(\\d\\d)(?:-?(\\d))?/;\n  const isoOrdinalRegex = /(\\d{4})-?(\\d{3})/;\n  const extractISOWeekData = simpleParse(\"weekYear\", \"weekNumber\", \"weekDay\");\n  const extractISOOrdinalData = simpleParse(\"year\", \"ordinal\");\n  const sqlYmdRegex = /(\\d{4})-(\\d\\d)-(\\d\\d)/; // dumbed-down version of the ISO one\n  const sqlTimeRegex = RegExp(\n    `${isoTimeBaseRegex.source} ?(?:${offsetRegex.source}|(${ianaRegex.source}))?`\n  );\n  const sqlTimeExtensionRegex = RegExp(`(?: ${sqlTimeRegex.source})?`);\n\n  function int(match, pos, fallback) {\n    const m = match[pos];\n    return isUndefined(m) ? fallback : parseInteger(m);\n  }\n\n  function extractISOYmd(match, cursor) {\n    const item = {\n      year: int(match, cursor),\n      month: int(match, cursor + 1, 1),\n      day: int(match, cursor + 2, 1),\n    };\n\n    return [item, null, cursor + 3];\n  }\n\n  function extractISOTime(match, cursor) {\n    const item = {\n      hours: int(match, cursor, 0),\n      minutes: int(match, cursor + 1, 0),\n      seconds: int(match, cursor + 2, 0),\n      milliseconds: parseMillis(match[cursor + 3]),\n    };\n\n    return [item, null, cursor + 4];\n  }\n\n  function extractISOOffset(match, cursor) {\n    const local = !match[cursor] && !match[cursor + 1],\n      fullOffset = signedOffset(match[cursor + 1], match[cursor + 2]),\n      zone = local ? null : FixedOffsetZone.instance(fullOffset);\n    return [{}, zone, cursor + 3];\n  }\n\n  function extractIANAZone(match, cursor) {\n    const zone = match[cursor] ? IANAZone.create(match[cursor]) : null;\n    return [{}, zone, cursor + 1];\n  }\n\n  // ISO time parsing\n\n  const isoTimeOnly = RegExp(`^T?${isoTimeBaseRegex.source}$`);\n\n  // ISO duration parsing\n\n  const isoDuration =\n    /^-?P(?:(?:(-?\\d{1,20}(?:\\.\\d{1,20})?)Y)?(?:(-?\\d{1,20}(?:\\.\\d{1,20})?)M)?(?:(-?\\d{1,20}(?:\\.\\d{1,20})?)W)?(?:(-?\\d{1,20}(?:\\.\\d{1,20})?)D)?(?:T(?:(-?\\d{1,20}(?:\\.\\d{1,20})?)H)?(?:(-?\\d{1,20}(?:\\.\\d{1,20})?)M)?(?:(-?\\d{1,20})(?:[.,](-?\\d{1,20}))?S)?)?)$/;\n\n  function extractISODuration(match) {\n    const [s, yearStr, monthStr, weekStr, dayStr, hourStr, minuteStr, secondStr, millisecondsStr] =\n      match;\n\n    const hasNegativePrefix = s[0] === \"-\";\n    const negativeSeconds = secondStr && secondStr[0] === \"-\";\n\n    const maybeNegate = (num, force = false) =>\n      num !== undefined && (force || (num && hasNegativePrefix)) ? -num : num;\n\n    return [\n      {\n        years: maybeNegate(parseFloating(yearStr)),\n        months: maybeNegate(parseFloating(monthStr)),\n        weeks: maybeNegate(parseFloating(weekStr)),\n        days: maybeNegate(parseFloating(dayStr)),\n        hours: maybeNegate(parseFloating(hourStr)),\n        minutes: maybeNegate(parseFloating(minuteStr)),\n        seconds: maybeNegate(parseFloating(secondStr), secondStr === \"-0\"),\n        milliseconds: maybeNegate(parseMillis(millisecondsStr), negativeSeconds),\n      },\n    ];\n  }\n\n  // These are a little braindead. EDT *should* tell us that we're in, say, America/New_York\n  // and not just that we're in -240 *right now*. But since I don't think these are used that often\n  // I'm just going to ignore that\n  const obsOffsets = {\n    GMT: 0,\n    EDT: -4 * 60,\n    EST: -5 * 60,\n    CDT: -5 * 60,\n    CST: -6 * 60,\n    MDT: -6 * 60,\n    MST: -7 * 60,\n    PDT: -7 * 60,\n    PST: -8 * 60,\n  };\n\n  function fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr) {\n    const result = {\n      year: yearStr.length === 2 ? untruncateYear(parseInteger(yearStr)) : parseInteger(yearStr),\n      month: monthsShort.indexOf(monthStr) + 1,\n      day: parseInteger(dayStr),\n      hour: parseInteger(hourStr),\n      minute: parseInteger(minuteStr),\n    };\n\n    if (secondStr) result.second = parseInteger(secondStr);\n    if (weekdayStr) {\n      result.weekday =\n        weekdayStr.length > 3\n          ? weekdaysLong.indexOf(weekdayStr) + 1\n          : weekdaysShort.indexOf(weekdayStr) + 1;\n    }\n\n    return result;\n  }\n\n  // RFC 2822/5322\n  const rfc2822 =\n    /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s)?(\\d{1,2})\\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s(\\d{2,4})\\s(\\d\\d):(\\d\\d)(?::(\\d\\d))?\\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|(?:([+-]\\d\\d)(\\d\\d)))$/;\n\n  function extractRFC2822(match) {\n    const [\n        ,\n        weekdayStr,\n        dayStr,\n        monthStr,\n        yearStr,\n        hourStr,\n        minuteStr,\n        secondStr,\n        obsOffset,\n        milOffset,\n        offHourStr,\n        offMinuteStr,\n      ] = match,\n      result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr);\n\n    let offset;\n    if (obsOffset) {\n      offset = obsOffsets[obsOffset];\n    } else if (milOffset) {\n      offset = 0;\n    } else {\n      offset = signedOffset(offHourStr, offMinuteStr);\n    }\n\n    return [result, new FixedOffsetZone(offset)];\n  }\n\n  function preprocessRFC2822(s) {\n    // Remove comments and folding whitespace and replace multiple-spaces with a single space\n    return s\n      .replace(/\\([^()]*\\)|[\\n\\t]/g, \" \")\n      .replace(/(\\s\\s+)/g, \" \")\n      .trim();\n  }\n\n  // http date\n\n  const rfc1123 =\n      /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\\d\\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\\d{4}) (\\d\\d):(\\d\\d):(\\d\\d) GMT$/,\n    rfc850 =\n      /^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\\d\\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\\d\\d) (\\d\\d):(\\d\\d):(\\d\\d) GMT$/,\n    ascii =\n      /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \\d|\\d\\d) (\\d\\d):(\\d\\d):(\\d\\d) (\\d{4})$/;\n\n  function extractRFC1123Or850(match) {\n    const [, weekdayStr, dayStr, monthStr, yearStr, hourStr, minuteStr, secondStr] = match,\n      result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr);\n    return [result, FixedOffsetZone.utcInstance];\n  }\n\n  function extractASCII(match) {\n    const [, weekdayStr, monthStr, dayStr, hourStr, minuteStr, secondStr, yearStr] = match,\n      result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr);\n    return [result, FixedOffsetZone.utcInstance];\n  }\n\n  const isoYmdWithTimeExtensionRegex = combineRegexes(isoYmdRegex, isoTimeExtensionRegex);\n  const isoWeekWithTimeExtensionRegex = combineRegexes(isoWeekRegex, isoTimeExtensionRegex);\n  const isoOrdinalWithTimeExtensionRegex = combineRegexes(isoOrdinalRegex, isoTimeExtensionRegex);\n  const isoTimeCombinedRegex = combineRegexes(isoTimeRegex);\n\n  const extractISOYmdTimeAndOffset = combineExtractors(\n    extractISOYmd,\n    extractISOTime,\n    extractISOOffset,\n    extractIANAZone\n  );\n  const extractISOWeekTimeAndOffset = combineExtractors(\n    extractISOWeekData,\n    extractISOTime,\n    extractISOOffset,\n    extractIANAZone\n  );\n  const extractISOOrdinalDateAndTime = combineExtractors(\n    extractISOOrdinalData,\n    extractISOTime,\n    extractISOOffset,\n    extractIANAZone\n  );\n  const extractISOTimeAndOffset = combineExtractors(\n    extractISOTime,\n    extractISOOffset,\n    extractIANAZone\n  );\n\n  /*\n   * @private\n   */\n\n  function parseISODate(s) {\n    return parse(\n      s,\n      [isoYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset],\n      [isoWeekWithTimeExtensionRegex, extractISOWeekTimeAndOffset],\n      [isoOrdinalWithTimeExtensionRegex, extractISOOrdinalDateAndTime],\n      [isoTimeCombinedRegex, extractISOTimeAndOffset]\n    );\n  }\n\n  function parseRFC2822Date(s) {\n    return parse(preprocessRFC2822(s), [rfc2822, extractRFC2822]);\n  }\n\n  function parseHTTPDate(s) {\n    return parse(\n      s,\n      [rfc1123, extractRFC1123Or850],\n      [rfc850, extractRFC1123Or850],\n      [ascii, extractASCII]\n    );\n  }\n\n  function parseISODuration(s) {\n    return parse(s, [isoDuration, extractISODuration]);\n  }\n\n  const extractISOTimeOnly = combineExtractors(extractISOTime);\n\n  function parseISOTimeOnly(s) {\n    return parse(s, [isoTimeOnly, extractISOTimeOnly]);\n  }\n\n  const sqlYmdWithTimeExtensionRegex = combineRegexes(sqlYmdRegex, sqlTimeExtensionRegex);\n  const sqlTimeCombinedRegex = combineRegexes(sqlTimeRegex);\n\n  const extractISOTimeOffsetAndIANAZone = combineExtractors(\n    extractISOTime,\n    extractISOOffset,\n    extractIANAZone\n  );\n\n  function parseSQL(s) {\n    return parse(\n      s,\n      [sqlYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset],\n      [sqlTimeCombinedRegex, extractISOTimeOffsetAndIANAZone]\n    );\n  }\n\n  const INVALID$2 = \"Invalid Duration\";\n\n  // unit conversion constants\n  const lowOrderMatrix = {\n      weeks: {\n        days: 7,\n        hours: 7 * 24,\n        minutes: 7 * 24 * 60,\n        seconds: 7 * 24 * 60 * 60,\n        milliseconds: 7 * 24 * 60 * 60 * 1000,\n      },\n      days: {\n        hours: 24,\n        minutes: 24 * 60,\n        seconds: 24 * 60 * 60,\n        milliseconds: 24 * 60 * 60 * 1000,\n      },\n      hours: { minutes: 60, seconds: 60 * 60, milliseconds: 60 * 60 * 1000 },\n      minutes: { seconds: 60, milliseconds: 60 * 1000 },\n      seconds: { milliseconds: 1000 },\n    },\n    casualMatrix = {\n      years: {\n        quarters: 4,\n        months: 12,\n        weeks: 52,\n        days: 365,\n        hours: 365 * 24,\n        minutes: 365 * 24 * 60,\n        seconds: 365 * 24 * 60 * 60,\n        milliseconds: 365 * 24 * 60 * 60 * 1000,\n      },\n      quarters: {\n        months: 3,\n        weeks: 13,\n        days: 91,\n        hours: 91 * 24,\n        minutes: 91 * 24 * 60,\n        seconds: 91 * 24 * 60 * 60,\n        milliseconds: 91 * 24 * 60 * 60 * 1000,\n      },\n      months: {\n        weeks: 4,\n        days: 30,\n        hours: 30 * 24,\n        minutes: 30 * 24 * 60,\n        seconds: 30 * 24 * 60 * 60,\n        milliseconds: 30 * 24 * 60 * 60 * 1000,\n      },\n\n      ...lowOrderMatrix,\n    },\n    daysInYearAccurate = 146097.0 / 400,\n    daysInMonthAccurate = 146097.0 / 4800,\n    accurateMatrix = {\n      years: {\n        quarters: 4,\n        months: 12,\n        weeks: daysInYearAccurate / 7,\n        days: daysInYearAccurate,\n        hours: daysInYearAccurate * 24,\n        minutes: daysInYearAccurate * 24 * 60,\n        seconds: daysInYearAccurate * 24 * 60 * 60,\n        milliseconds: daysInYearAccurate * 24 * 60 * 60 * 1000,\n      },\n      quarters: {\n        months: 3,\n        weeks: daysInYearAccurate / 28,\n        days: daysInYearAccurate / 4,\n        hours: (daysInYearAccurate * 24) / 4,\n        minutes: (daysInYearAccurate * 24 * 60) / 4,\n        seconds: (daysInYearAccurate * 24 * 60 * 60) / 4,\n        milliseconds: (daysInYearAccurate * 24 * 60 * 60 * 1000) / 4,\n      },\n      months: {\n        weeks: daysInMonthAccurate / 7,\n        days: daysInMonthAccurate,\n        hours: daysInMonthAccurate * 24,\n        minutes: daysInMonthAccurate * 24 * 60,\n        seconds: daysInMonthAccurate * 24 * 60 * 60,\n        milliseconds: daysInMonthAccurate * 24 * 60 * 60 * 1000,\n      },\n      ...lowOrderMatrix,\n    };\n\n  // units ordered by size\n  const orderedUnits$1 = [\n    \"years\",\n    \"quarters\",\n    \"months\",\n    \"weeks\",\n    \"days\",\n    \"hours\",\n    \"minutes\",\n    \"seconds\",\n    \"milliseconds\",\n  ];\n\n  const reverseUnits = orderedUnits$1.slice(0).reverse();\n\n  // clone really means \"create another instance just like this one, but with these changes\"\n  function clone$1(dur, alts, clear = false) {\n    // deep merge for vals\n    const conf = {\n      values: clear ? alts.values : { ...dur.values, ...(alts.values || {}) },\n      loc: dur.loc.clone(alts.loc),\n      conversionAccuracy: alts.conversionAccuracy || dur.conversionAccuracy,\n      matrix: alts.matrix || dur.matrix,\n    };\n    return new Duration(conf);\n  }\n\n  function durationToMillis(matrix, vals) {\n    let sum = vals.milliseconds ?? 0;\n    for (const unit of reverseUnits.slice(1)) {\n      if (vals[unit]) {\n        sum += vals[unit] * matrix[unit][\"milliseconds\"];\n      }\n    }\n    return sum;\n  }\n\n  // NB: mutates parameters\n  function normalizeValues(matrix, vals) {\n    // the logic below assumes the overall value of the duration is positive\n    // if this is not the case, factor is used to make it so\n    const factor = durationToMillis(matrix, vals) < 0 ? -1 : 1;\n\n    orderedUnits$1.reduceRight((previous, current) => {\n      if (!isUndefined(vals[current])) {\n        if (previous) {\n          const previousVal = vals[previous] * factor;\n          const conv = matrix[current][previous];\n\n          // if (previousVal < 0):\n          // lower order unit is negative (e.g. { years: 2, days: -2 })\n          // normalize this by reducing the higher order unit by the appropriate amount\n          // and increasing the lower order unit\n          // this can never make the higher order unit negative, because this function only operates\n          // on positive durations, so the amount of time represented by the lower order unit cannot\n          // be larger than the higher order unit\n          // else:\n          // lower order unit is positive (e.g. { years: 2, days: 450 } or { years: -2, days: 450 })\n          // in this case we attempt to convert as much as possible from the lower order unit into\n          // the higher order one\n          //\n          // Math.floor takes care of both of these cases, rounding away from 0\n          // if previousVal < 0 it makes the absolute value larger\n          // if previousVal >= it makes the absolute value smaller\n          const rollUp = Math.floor(previousVal / conv);\n          vals[current] += rollUp * factor;\n          vals[previous] -= rollUp * conv * factor;\n        }\n        return current;\n      } else {\n        return previous;\n      }\n    }, null);\n\n    // try to convert any decimals into smaller units if possible\n    // for example for { years: 2.5, days: 0, seconds: 0 } we want to get { years: 2, days: 182, hours: 12 }\n    orderedUnits$1.reduce((previous, current) => {\n      if (!isUndefined(vals[current])) {\n        if (previous) {\n          const fraction = vals[previous] % 1;\n          vals[previous] -= fraction;\n          vals[current] += fraction * matrix[previous][current];\n        }\n        return current;\n      } else {\n        return previous;\n      }\n    }, null);\n  }\n\n  // Remove all properties with a value of 0 from an object\n  function removeZeroes(vals) {\n    const newVals = {};\n    for (const [key, value] of Object.entries(vals)) {\n      if (value !== 0) {\n        newVals[key] = value;\n      }\n    }\n    return newVals;\n  }\n\n  /**\n   * A Duration object represents a period of time, like \"2 months\" or \"1 day, 1 hour\". Conceptually, it's just a map of units to their quantities, accompanied by some additional configuration and methods for creating, parsing, interrogating, transforming, and formatting them. They can be used on their own or in conjunction with other Luxon types; for example, you can use {@link DateTime#plus} to add a Duration object to a DateTime, producing another DateTime.\n   *\n   * Here is a brief overview of commonly used methods and getters in Duration:\n   *\n   * * **Creation** To create a Duration, use {@link Duration.fromMillis}, {@link Duration.fromObject}, or {@link Duration.fromISO}.\n   * * **Unit values** See the {@link Duration#years}, {@link Duration#months}, {@link Duration#weeks}, {@link Duration#days}, {@link Duration#hours}, {@link Duration#minutes}, {@link Duration#seconds}, {@link Duration#milliseconds} accessors.\n   * * **Configuration** See  {@link Duration#locale} and {@link Duration#numberingSystem} accessors.\n   * * **Transformation** To create new Durations out of old ones use {@link Duration#plus}, {@link Duration#minus}, {@link Duration#normalize}, {@link Duration#set}, {@link Duration#reconfigure}, {@link Duration#shiftTo}, and {@link Duration#negate}.\n   * * **Output** To convert the Duration into other representations, see {@link Duration#as}, {@link Duration#toISO}, {@link Duration#toFormat}, and {@link Duration#toJSON}\n   *\n   * There's are more methods documented below. In addition, for more information on subtler topics like internationalization and validity, see the external documentation.\n   */\n  class Duration {\n    /**\n     * @private\n     */\n    constructor(config) {\n      const accurate = config.conversionAccuracy === \"longterm\" || false;\n      let matrix = accurate ? accurateMatrix : casualMatrix;\n\n      if (config.matrix) {\n        matrix = config.matrix;\n      }\n\n      /**\n       * @access private\n       */\n      this.values = config.values;\n      /**\n       * @access private\n       */\n      this.loc = config.loc || Locale.create();\n      /**\n       * @access private\n       */\n      this.conversionAccuracy = accurate ? \"longterm\" : \"casual\";\n      /**\n       * @access private\n       */\n      this.invalid = config.invalid || null;\n      /**\n       * @access private\n       */\n      this.matrix = matrix;\n      /**\n       * @access private\n       */\n      this.isLuxonDuration = true;\n    }\n\n    /**\n     * Create Duration from a number of milliseconds.\n     * @param {number} count of milliseconds\n     * @param {Object} opts - options for parsing\n     * @param {string} [opts.locale='en-US'] - the locale to use\n     * @param {string} opts.numberingSystem - the numbering system to use\n     * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use\n     * @return {Duration}\n     */\n    static fromMillis(count, opts) {\n      return Duration.fromObject({ milliseconds: count }, opts);\n    }\n\n    /**\n     * Create a Duration from a JavaScript object with keys like 'years' and 'hours'.\n     * If this object is empty then a zero milliseconds duration is returned.\n     * @param {Object} obj - the object to create the DateTime from\n     * @param {number} obj.years\n     * @param {number} obj.quarters\n     * @param {number} obj.months\n     * @param {number} obj.weeks\n     * @param {number} obj.days\n     * @param {number} obj.hours\n     * @param {number} obj.minutes\n     * @param {number} obj.seconds\n     * @param {number} obj.milliseconds\n     * @param {Object} [opts=[]] - options for creating this Duration\n     * @param {string} [opts.locale='en-US'] - the locale to use\n     * @param {string} opts.numberingSystem - the numbering system to use\n     * @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use\n     * @param {string} [opts.matrix=Object] - the custom conversion system to use\n     * @return {Duration}\n     */\n    static fromObject(obj, opts = {}) {\n      if (obj == null || typeof obj !== \"object\") {\n        throw new InvalidArgumentError(\n          `Duration.fromObject: argument expected to be an object, got ${\n          obj === null ? \"null\" : typeof obj\n        }`\n        );\n      }\n\n      return new Duration({\n        values: normalizeObject(obj, Duration.normalizeUnit),\n        loc: Locale.fromObject(opts),\n        conversionAccuracy: opts.conversionAccuracy,\n        matrix: opts.matrix,\n      });\n    }\n\n    /**\n     * Create a Duration from DurationLike.\n     *\n     * @param {Object | number | Duration} durationLike\n     * One of:\n     * - object with keys like 'years' and 'hours'.\n     * - number representing milliseconds\n     * - Duration instance\n     * @return {Duration}\n     */\n    static fromDurationLike(durationLike) {\n      if (isNumber(durationLike)) {\n        return Duration.fromMillis(durationLike);\n      } else if (Duration.isDuration(durationLike)) {\n        return durationLike;\n      } else if (typeof durationLike === \"object\") {\n        return Duration.fromObject(durationLike);\n      } else {\n        throw new InvalidArgumentError(\n          `Unknown duration argument ${durationLike} of type ${typeof durationLike}`\n        );\n      }\n    }\n\n    /**\n     * Create a Duration from an ISO 8601 duration string.\n     * @param {string} text - text to parse\n     * @param {Object} opts - options for parsing\n     * @param {string} [opts.locale='en-US'] - the locale to use\n     * @param {string} opts.numberingSystem - the numbering system to use\n     * @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use\n     * @param {string} [opts.matrix=Object] - the preset conversion system to use\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Durations\n     * @example Duration.fromISO('P3Y6M1W4DT12H30M5S').toObject() //=> { years: 3, months: 6, weeks: 1, days: 4, hours: 12, minutes: 30, seconds: 5 }\n     * @example Duration.fromISO('PT23H').toObject() //=> { hours: 23 }\n     * @example Duration.fromISO('P5Y3M').toObject() //=> { years: 5, months: 3 }\n     * @return {Duration}\n     */\n    static fromISO(text, opts) {\n      const [parsed] = parseISODuration(text);\n      if (parsed) {\n        return Duration.fromObject(parsed, opts);\n      } else {\n        return Duration.invalid(\"unparsable\", `the input \"${text}\" can't be parsed as ISO 8601`);\n      }\n    }\n\n    /**\n     * Create a Duration from an ISO 8601 time string.\n     * @param {string} text - text to parse\n     * @param {Object} opts - options for parsing\n     * @param {string} [opts.locale='en-US'] - the locale to use\n     * @param {string} opts.numberingSystem - the numbering system to use\n     * @param {string} [opts.conversionAccuracy='casual'] - the preset conversion system to use\n     * @param {string} [opts.matrix=Object] - the conversion system to use\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Times\n     * @example Duration.fromISOTime('11:22:33.444').toObject() //=> { hours: 11, minutes: 22, seconds: 33, milliseconds: 444 }\n     * @example Duration.fromISOTime('11:00').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }\n     * @example Duration.fromISOTime('T11:00').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }\n     * @example Duration.fromISOTime('1100').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }\n     * @example Duration.fromISOTime('T1100').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }\n     * @return {Duration}\n     */\n    static fromISOTime(text, opts) {\n      const [parsed] = parseISOTimeOnly(text);\n      if (parsed) {\n        return Duration.fromObject(parsed, opts);\n      } else {\n        return Duration.invalid(\"unparsable\", `the input \"${text}\" can't be parsed as ISO 8601`);\n      }\n    }\n\n    /**\n     * Create an invalid Duration.\n     * @param {string} reason - simple string of why this datetime is invalid. Should not contain parameters or anything else data-dependent\n     * @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information\n     * @return {Duration}\n     */\n    static invalid(reason, explanation = null) {\n      if (!reason) {\n        throw new InvalidArgumentError(\"need to specify a reason the Duration is invalid\");\n      }\n\n      const invalid = reason instanceof Invalid ? reason : new Invalid(reason, explanation);\n\n      if (Settings.throwOnInvalid) {\n        throw new InvalidDurationError(invalid);\n      } else {\n        return new Duration({ invalid });\n      }\n    }\n\n    /**\n     * @private\n     */\n    static normalizeUnit(unit) {\n      const normalized = {\n        year: \"years\",\n        years: \"years\",\n        quarter: \"quarters\",\n        quarters: \"quarters\",\n        month: \"months\",\n        months: \"months\",\n        week: \"weeks\",\n        weeks: \"weeks\",\n        day: \"days\",\n        days: \"days\",\n        hour: \"hours\",\n        hours: \"hours\",\n        minute: \"minutes\",\n        minutes: \"minutes\",\n        second: \"seconds\",\n        seconds: \"seconds\",\n        millisecond: \"milliseconds\",\n        milliseconds: \"milliseconds\",\n      }[unit ? unit.toLowerCase() : unit];\n\n      if (!normalized) throw new InvalidUnitError(unit);\n\n      return normalized;\n    }\n\n    /**\n     * Check if an object is a Duration. Works across context boundaries\n     * @param {object} o\n     * @return {boolean}\n     */\n    static isDuration(o) {\n      return (o && o.isLuxonDuration) || false;\n    }\n\n    /**\n     * Get  the locale of a Duration, such 'en-GB'\n     * @type {string}\n     */\n    get locale() {\n      return this.isValid ? this.loc.locale : null;\n    }\n\n    /**\n     * Get the numbering system of a Duration, such 'beng'. The numbering system is used when formatting the Duration\n     *\n     * @type {string}\n     */\n    get numberingSystem() {\n      return this.isValid ? this.loc.numberingSystem : null;\n    }\n\n    /**\n     * Returns a string representation of this Duration formatted according to the specified format string. You may use these tokens:\n     * * `S` for milliseconds\n     * * `s` for seconds\n     * * `m` for minutes\n     * * `h` for hours\n     * * `d` for days\n     * * `w` for weeks\n     * * `M` for months\n     * * `y` for years\n     * Notes:\n     * * Add padding by repeating the token, e.g. \"yy\" pads the years to two digits, \"hhhh\" pads the hours out to four digits\n     * * Tokens can be escaped by wrapping with single quotes.\n     * * The duration will be converted to the set of units in the format string using {@link Duration#shiftTo} and the Durations's conversion accuracy setting.\n     * @param {string} fmt - the format string\n     * @param {Object} opts - options\n     * @param {boolean} [opts.floor=true] - floor numerical values\n     * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat(\"y d s\") //=> \"1 6 2\"\n     * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat(\"yy dd sss\") //=> \"01 06 002\"\n     * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat(\"M S\") //=> \"12 518402000\"\n     * @return {string}\n     */\n    toFormat(fmt, opts = {}) {\n      // reverse-compat since 1.2; we always round down now, never up, and we do it by default\n      const fmtOpts = {\n        ...opts,\n        floor: opts.round !== false && opts.floor !== false,\n      };\n      return this.isValid\n        ? Formatter.create(this.loc, fmtOpts).formatDurationFromString(this, fmt)\n        : INVALID$2;\n    }\n\n    /**\n     * Returns a string representation of a Duration with all units included.\n     * To modify its behavior, use `listStyle` and any Intl.NumberFormat option, though `unitDisplay` is especially relevant.\n     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options\n     * @param {Object} opts - Formatting options. Accepts the same keys as the options parameter of the native `Intl.NumberFormat` constructor, as well as `listStyle`.\n     * @param {string} [opts.listStyle='narrow'] - How to format the merged list. Corresponds to the `style` property of the options parameter of the native `Intl.ListFormat` constructor.\n     * @example\n     * ```js\n     * var dur = Duration.fromObject({ days: 1, hours: 5, minutes: 6 })\n     * dur.toHuman() //=> '1 day, 5 hours, 6 minutes'\n     * dur.toHuman({ listStyle: \"long\" }) //=> '1 day, 5 hours, and 6 minutes'\n     * dur.toHuman({ unitDisplay: \"short\" }) //=> '1 day, 5 hr, 6 min'\n     * ```\n     */\n    toHuman(opts = {}) {\n      if (!this.isValid) return INVALID$2;\n\n      const l = orderedUnits$1\n        .map((unit) => {\n          const val = this.values[unit];\n          if (isUndefined(val)) {\n            return null;\n          }\n          return this.loc\n            .numberFormatter({ style: \"unit\", unitDisplay: \"long\", ...opts, unit: unit.slice(0, -1) })\n            .format(val);\n        })\n        .filter((n) => n);\n\n      return this.loc\n        .listFormatter({ type: \"conjunction\", style: opts.listStyle || \"narrow\", ...opts })\n        .format(l);\n    }\n\n    /**\n     * Returns a JavaScript object with this Duration's values.\n     * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toObject() //=> { years: 1, days: 6, seconds: 2 }\n     * @return {Object}\n     */\n    toObject() {\n      if (!this.isValid) return {};\n      return { ...this.values };\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of this Duration.\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Durations\n     * @example Duration.fromObject({ years: 3, seconds: 45 }).toISO() //=> 'P3YT45S'\n     * @example Duration.fromObject({ months: 4, seconds: 45 }).toISO() //=> 'P4MT45S'\n     * @example Duration.fromObject({ months: 5 }).toISO() //=> 'P5M'\n     * @example Duration.fromObject({ minutes: 5 }).toISO() //=> 'PT5M'\n     * @example Duration.fromObject({ milliseconds: 6 }).toISO() //=> 'PT0.006S'\n     * @return {string}\n     */\n    toISO() {\n      // we could use the formatter, but this is an easier way to get the minimum string\n      if (!this.isValid) return null;\n\n      let s = \"P\";\n      if (this.years !== 0) s += this.years + \"Y\";\n      if (this.months !== 0 || this.quarters !== 0) s += this.months + this.quarters * 3 + \"M\";\n      if (this.weeks !== 0) s += this.weeks + \"W\";\n      if (this.days !== 0) s += this.days + \"D\";\n      if (this.hours !== 0 || this.minutes !== 0 || this.seconds !== 0 || this.milliseconds !== 0)\n        s += \"T\";\n      if (this.hours !== 0) s += this.hours + \"H\";\n      if (this.minutes !== 0) s += this.minutes + \"M\";\n      if (this.seconds !== 0 || this.milliseconds !== 0)\n        // this will handle \"floating point madness\" by removing extra decimal places\n        // https://stackoverflow.com/questions/588004/is-floating-point-math-broken\n        s += roundTo(this.seconds + this.milliseconds / 1000, 3) + \"S\";\n      if (s === \"P\") s += \"T0S\";\n      return s;\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of this Duration, formatted as a time of day.\n     * Note that this will return null if the duration is invalid, negative, or equal to or greater than 24 hours.\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Times\n     * @param {Object} opts - options\n     * @param {boolean} [opts.suppressMilliseconds=false] - exclude milliseconds from the format if they're 0\n     * @param {boolean} [opts.suppressSeconds=false] - exclude seconds from the format if they're 0\n     * @param {boolean} [opts.includePrefix=false] - include the `T` prefix\n     * @param {string} [opts.format='extended'] - choose between the basic and extended format\n     * @example Duration.fromObject({ hours: 11 }).toISOTime() //=> '11:00:00.000'\n     * @example Duration.fromObject({ hours: 11 }).toISOTime({ suppressMilliseconds: true }) //=> '11:00:00'\n     * @example Duration.fromObject({ hours: 11 }).toISOTime({ suppressSeconds: true }) //=> '11:00'\n     * @example Duration.fromObject({ hours: 11 }).toISOTime({ includePrefix: true }) //=> 'T11:00:00.000'\n     * @example Duration.fromObject({ hours: 11 }).toISOTime({ format: 'basic' }) //=> '110000.000'\n     * @return {string}\n     */\n    toISOTime(opts = {}) {\n      if (!this.isValid) return null;\n\n      const millis = this.toMillis();\n      if (millis < 0 || millis >= 86400000) return null;\n\n      opts = {\n        suppressMilliseconds: false,\n        suppressSeconds: false,\n        includePrefix: false,\n        format: \"extended\",\n        ...opts,\n        includeOffset: false,\n      };\n\n      const dateTime = DateTime.fromMillis(millis, { zone: \"UTC\" });\n      return dateTime.toISOTime(opts);\n    }\n\n    /**\n     * Returns an ISO 8601 representation of this Duration appropriate for use in JSON.\n     * @return {string}\n     */\n    toJSON() {\n      return this.toISO();\n    }\n\n    /**\n     * Returns an ISO 8601 representation of this Duration appropriate for use in debugging.\n     * @return {string}\n     */\n    toString() {\n      return this.toISO();\n    }\n\n    /**\n     * Returns a string representation of this Duration appropriate for the REPL.\n     * @return {string}\n     */\n    [Symbol.for(\"nodejs.util.inspect.custom\")]() {\n      if (this.isValid) {\n        return `Duration { values: ${JSON.stringify(this.values)} }`;\n      } else {\n        return `Duration { Invalid, reason: ${this.invalidReason} }`;\n      }\n    }\n\n    /**\n     * Returns an milliseconds value of this Duration.\n     * @return {number}\n     */\n    toMillis() {\n      if (!this.isValid) return NaN;\n\n      return durationToMillis(this.matrix, this.values);\n    }\n\n    /**\n     * Returns an milliseconds value of this Duration. Alias of {@link toMillis}\n     * @return {number}\n     */\n    valueOf() {\n      return this.toMillis();\n    }\n\n    /**\n     * Make this Duration longer by the specified amount. Return a newly-constructed Duration.\n     * @param {Duration|Object|number} duration - The amount to add. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject()\n     * @return {Duration}\n     */\n    plus(duration) {\n      if (!this.isValid) return this;\n\n      const dur = Duration.fromDurationLike(duration),\n        result = {};\n\n      for (const k of orderedUnits$1) {\n        if (hasOwnProperty(dur.values, k) || hasOwnProperty(this.values, k)) {\n          result[k] = dur.get(k) + this.get(k);\n        }\n      }\n\n      return clone$1(this, { values: result }, true);\n    }\n\n    /**\n     * Make this Duration shorter by the specified amount. Return a newly-constructed Duration.\n     * @param {Duration|Object|number} duration - The amount to subtract. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject()\n     * @return {Duration}\n     */\n    minus(duration) {\n      if (!this.isValid) return this;\n\n      const dur = Duration.fromDurationLike(duration);\n      return this.plus(dur.negate());\n    }\n\n    /**\n     * Scale this Duration by the specified amount. Return a newly-constructed Duration.\n     * @param {function} fn - The function to apply to each unit. Arity is 1 or 2: the value of the unit and, optionally, the unit name. Must return a number.\n     * @example Duration.fromObject({ hours: 1, minutes: 30 }).mapUnits(x => x * 2) //=> { hours: 2, minutes: 60 }\n     * @example Duration.fromObject({ hours: 1, minutes: 30 }).mapUnits((x, u) => u === \"hours\" ? x * 2 : x) //=> { hours: 2, minutes: 30 }\n     * @return {Duration}\n     */\n    mapUnits(fn) {\n      if (!this.isValid) return this;\n      const result = {};\n      for (const k of Object.keys(this.values)) {\n        result[k] = asNumber(fn(this.values[k], k));\n      }\n      return clone$1(this, { values: result }, true);\n    }\n\n    /**\n     * Get the value of unit.\n     * @param {string} unit - a unit such as 'minute' or 'day'\n     * @example Duration.fromObject({years: 2, days: 3}).get('years') //=> 2\n     * @example Duration.fromObject({years: 2, days: 3}).get('months') //=> 0\n     * @example Duration.fromObject({years: 2, days: 3}).get('days') //=> 3\n     * @return {number}\n     */\n    get(unit) {\n      return this[Duration.normalizeUnit(unit)];\n    }\n\n    /**\n     * \"Set\" the values of specified units. Return a newly-constructed Duration.\n     * @param {Object} values - a mapping of units to numbers\n     * @example dur.set({ years: 2017 })\n     * @example dur.set({ hours: 8, minutes: 30 })\n     * @return {Duration}\n     */\n    set(values) {\n      if (!this.isValid) return this;\n\n      const mixed = { ...this.values, ...normalizeObject(values, Duration.normalizeUnit) };\n      return clone$1(this, { values: mixed });\n    }\n\n    /**\n     * \"Set\" the locale and/or numberingSystem.  Returns a newly-constructed Duration.\n     * @example dur.reconfigure({ locale: 'en-GB' })\n     * @return {Duration}\n     */\n    reconfigure({ locale, numberingSystem, conversionAccuracy, matrix } = {}) {\n      const loc = this.loc.clone({ locale, numberingSystem });\n      const opts = { loc, matrix, conversionAccuracy };\n      return clone$1(this, opts);\n    }\n\n    /**\n     * Return the length of the duration in the specified unit.\n     * @param {string} unit - a unit such as 'minutes' or 'days'\n     * @example Duration.fromObject({years: 1}).as('days') //=> 365\n     * @example Duration.fromObject({years: 1}).as('months') //=> 12\n     * @example Duration.fromObject({hours: 60}).as('days') //=> 2.5\n     * @return {number}\n     */\n    as(unit) {\n      return this.isValid ? this.shiftTo(unit).get(unit) : NaN;\n    }\n\n    /**\n     * Reduce this Duration to its canonical representation in its current units.\n     * Assuming the overall value of the Duration is positive, this means:\n     * - excessive values for lower-order units are converted to higher-order units (if possible, see first and second example)\n     * - negative lower-order units are converted to higher order units (there must be such a higher order unit, otherwise\n     *   the overall value would be negative, see third example)\n     * - fractional values for higher-order units are converted to lower-order units (if possible, see fourth example)\n     *\n     * If the overall value is negative, the result of this method is equivalent to `this.negate().normalize().negate()`.\n     * @example Duration.fromObject({ years: 2, days: 5000 }).normalize().toObject() //=> { years: 15, days: 255 }\n     * @example Duration.fromObject({ days: 5000 }).normalize().toObject() //=> { days: 5000 }\n     * @example Duration.fromObject({ hours: 12, minutes: -45 }).normalize().toObject() //=> { hours: 11, minutes: 15 }\n     * @example Duration.fromObject({ years: 2.5, days: 0, hours: 0 }).normalize().toObject() //=> { years: 2, days: 182, hours: 12 }\n     * @return {Duration}\n     */\n    normalize() {\n      if (!this.isValid) return this;\n      const vals = this.toObject();\n      normalizeValues(this.matrix, vals);\n      return clone$1(this, { values: vals }, true);\n    }\n\n    /**\n     * Rescale units to its largest representation\n     * @example Duration.fromObject({ milliseconds: 90000 }).rescale().toObject() //=> { minutes: 1, seconds: 30 }\n     * @return {Duration}\n     */\n    rescale() {\n      if (!this.isValid) return this;\n      const vals = removeZeroes(this.normalize().shiftToAll().toObject());\n      return clone$1(this, { values: vals }, true);\n    }\n\n    /**\n     * Convert this Duration into its representation in a different set of units.\n     * @example Duration.fromObject({ hours: 1, seconds: 30 }).shiftTo('minutes', 'milliseconds').toObject() //=> { minutes: 60, milliseconds: 30000 }\n     * @return {Duration}\n     */\n    shiftTo(...units) {\n      if (!this.isValid) return this;\n\n      if (units.length === 0) {\n        return this;\n      }\n\n      units = units.map((u) => Duration.normalizeUnit(u));\n\n      const built = {},\n        accumulated = {},\n        vals = this.toObject();\n      let lastUnit;\n\n      for (const k of orderedUnits$1) {\n        if (units.indexOf(k) >= 0) {\n          lastUnit = k;\n\n          let own = 0;\n\n          // anything we haven't boiled down yet should get boiled to this unit\n          for (const ak in accumulated) {\n            own += this.matrix[ak][k] * accumulated[ak];\n            accumulated[ak] = 0;\n          }\n\n          // plus anything that's already in this unit\n          if (isNumber(vals[k])) {\n            own += vals[k];\n          }\n\n          // only keep the integer part for now in the hopes of putting any decimal part\n          // into a smaller unit later\n          const i = Math.trunc(own);\n          built[k] = i;\n          accumulated[k] = (own * 1000 - i * 1000) / 1000;\n\n          // otherwise, keep it in the wings to boil it later\n        } else if (isNumber(vals[k])) {\n          accumulated[k] = vals[k];\n        }\n      }\n\n      // anything leftover becomes the decimal for the last unit\n      // lastUnit must be defined since units is not empty\n      for (const key in accumulated) {\n        if (accumulated[key] !== 0) {\n          built[lastUnit] +=\n            key === lastUnit ? accumulated[key] : accumulated[key] / this.matrix[lastUnit][key];\n        }\n      }\n\n      normalizeValues(this.matrix, built);\n      return clone$1(this, { values: built }, true);\n    }\n\n    /**\n     * Shift this Duration to all available units.\n     * Same as shiftTo(\"years\", \"months\", \"weeks\", \"days\", \"hours\", \"minutes\", \"seconds\", \"milliseconds\")\n     * @return {Duration}\n     */\n    shiftToAll() {\n      if (!this.isValid) return this;\n      return this.shiftTo(\n        \"years\",\n        \"months\",\n        \"weeks\",\n        \"days\",\n        \"hours\",\n        \"minutes\",\n        \"seconds\",\n        \"milliseconds\"\n      );\n    }\n\n    /**\n     * Return the negative of this Duration.\n     * @example Duration.fromObject({ hours: 1, seconds: 30 }).negate().toObject() //=> { hours: -1, seconds: -30 }\n     * @return {Duration}\n     */\n    negate() {\n      if (!this.isValid) return this;\n      const negated = {};\n      for (const k of Object.keys(this.values)) {\n        negated[k] = this.values[k] === 0 ? 0 : -this.values[k];\n      }\n      return clone$1(this, { values: negated }, true);\n    }\n\n    /**\n     * Get the years.\n     * @type {number}\n     */\n    get years() {\n      return this.isValid ? this.values.years || 0 : NaN;\n    }\n\n    /**\n     * Get the quarters.\n     * @type {number}\n     */\n    get quarters() {\n      return this.isValid ? this.values.quarters || 0 : NaN;\n    }\n\n    /**\n     * Get the months.\n     * @type {number}\n     */\n    get months() {\n      return this.isValid ? this.values.months || 0 : NaN;\n    }\n\n    /**\n     * Get the weeks\n     * @type {number}\n     */\n    get weeks() {\n      return this.isValid ? this.values.weeks || 0 : NaN;\n    }\n\n    /**\n     * Get the days.\n     * @type {number}\n     */\n    get days() {\n      return this.isValid ? this.values.days || 0 : NaN;\n    }\n\n    /**\n     * Get the hours.\n     * @type {number}\n     */\n    get hours() {\n      return this.isValid ? this.values.hours || 0 : NaN;\n    }\n\n    /**\n     * Get the minutes.\n     * @type {number}\n     */\n    get minutes() {\n      return this.isValid ? this.values.minutes || 0 : NaN;\n    }\n\n    /**\n     * Get the seconds.\n     * @return {number}\n     */\n    get seconds() {\n      return this.isValid ? this.values.seconds || 0 : NaN;\n    }\n\n    /**\n     * Get the milliseconds.\n     * @return {number}\n     */\n    get milliseconds() {\n      return this.isValid ? this.values.milliseconds || 0 : NaN;\n    }\n\n    /**\n     * Returns whether the Duration is invalid. Invalid durations are returned by diff operations\n     * on invalid DateTimes or Intervals.\n     * @return {boolean}\n     */\n    get isValid() {\n      return this.invalid === null;\n    }\n\n    /**\n     * Returns an error code if this Duration became invalid, or null if the Duration is valid\n     * @return {string}\n     */\n    get invalidReason() {\n      return this.invalid ? this.invalid.reason : null;\n    }\n\n    /**\n     * Returns an explanation of why this Duration became invalid, or null if the Duration is valid\n     * @type {string}\n     */\n    get invalidExplanation() {\n      return this.invalid ? this.invalid.explanation : null;\n    }\n\n    /**\n     * Equality check\n     * Two Durations are equal iff they have the same units and the same values for each unit.\n     * @param {Duration} other\n     * @return {boolean}\n     */\n    equals(other) {\n      if (!this.isValid || !other.isValid) {\n        return false;\n      }\n\n      if (!this.loc.equals(other.loc)) {\n        return false;\n      }\n\n      function eq(v1, v2) {\n        // Consider 0 and undefined as equal\n        if (v1 === undefined || v1 === 0) return v2 === undefined || v2 === 0;\n        return v1 === v2;\n      }\n\n      for (const u of orderedUnits$1) {\n        if (!eq(this.values[u], other.values[u])) {\n          return false;\n        }\n      }\n      return true;\n    }\n  }\n\n  const INVALID$1 = \"Invalid Interval\";\n\n  // checks if the start is equal to or before the end\n  function validateStartEnd(start, end) {\n    if (!start || !start.isValid) {\n      return Interval.invalid(\"missing or invalid start\");\n    } else if (!end || !end.isValid) {\n      return Interval.invalid(\"missing or invalid end\");\n    } else if (end < start) {\n      return Interval.invalid(\n        \"end before start\",\n        `The end of an interval must be after its start, but you had start=${start.toISO()} and end=${end.toISO()}`\n      );\n    } else {\n      return null;\n    }\n  }\n\n  /**\n   * An Interval object represents a half-open interval of time, where each endpoint is a {@link DateTime}. Conceptually, it's a container for those two endpoints, accompanied by methods for creating, parsing, interrogating, comparing, transforming, and formatting them.\n   *\n   * Here is a brief overview of the most commonly used methods and getters in Interval:\n   *\n   * * **Creation** To create an Interval, use {@link Interval.fromDateTimes}, {@link Interval.after}, {@link Interval.before}, or {@link Interval.fromISO}.\n   * * **Accessors** Use {@link Interval#start} and {@link Interval#end} to get the start and end.\n   * * **Interrogation** To analyze the Interval, use {@link Interval#count}, {@link Interval#length}, {@link Interval#hasSame}, {@link Interval#contains}, {@link Interval#isAfter}, or {@link Interval#isBefore}.\n   * * **Transformation** To create other Intervals out of this one, use {@link Interval#set}, {@link Interval#splitAt}, {@link Interval#splitBy}, {@link Interval#divideEqually}, {@link Interval.merge}, {@link Interval.xor}, {@link Interval#union}, {@link Interval#intersection}, or {@link Interval#difference}.\n   * * **Comparison** To compare this Interval to another one, use {@link Interval#equals}, {@link Interval#overlaps}, {@link Interval#abutsStart}, {@link Interval#abutsEnd}, {@link Interval#engulfs}\n   * * **Output** To convert the Interval into other representations, see {@link Interval#toString}, {@link Interval#toLocaleString}, {@link Interval#toISO}, {@link Interval#toISODate}, {@link Interval#toISOTime}, {@link Interval#toFormat}, and {@link Interval#toDuration}.\n   */\n  class Interval {\n    /**\n     * @private\n     */\n    constructor(config) {\n      /**\n       * @access private\n       */\n      this.s = config.start;\n      /**\n       * @access private\n       */\n      this.e = config.end;\n      /**\n       * @access private\n       */\n      this.invalid = config.invalid || null;\n      /**\n       * @access private\n       */\n      this.isLuxonInterval = true;\n    }\n\n    /**\n     * Create an invalid Interval.\n     * @param {string} reason - simple string of why this Interval is invalid. Should not contain parameters or anything else data-dependent\n     * @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information\n     * @return {Interval}\n     */\n    static invalid(reason, explanation = null) {\n      if (!reason) {\n        throw new InvalidArgumentError(\"need to specify a reason the Interval is invalid\");\n      }\n\n      const invalid = reason instanceof Invalid ? reason : new Invalid(reason, explanation);\n\n      if (Settings.throwOnInvalid) {\n        throw new InvalidIntervalError(invalid);\n      } else {\n        return new Interval({ invalid });\n      }\n    }\n\n    /**\n     * Create an Interval from a start DateTime and an end DateTime. Inclusive of the start but not the end.\n     * @param {DateTime|Date|Object} start\n     * @param {DateTime|Date|Object} end\n     * @return {Interval}\n     */\n    static fromDateTimes(start, end) {\n      const builtStart = friendlyDateTime(start),\n        builtEnd = friendlyDateTime(end);\n\n      const validateError = validateStartEnd(builtStart, builtEnd);\n\n      if (validateError == null) {\n        return new Interval({\n          start: builtStart,\n          end: builtEnd,\n        });\n      } else {\n        return validateError;\n      }\n    }\n\n    /**\n     * Create an Interval from a start DateTime and a Duration to extend to.\n     * @param {DateTime|Date|Object} start\n     * @param {Duration|Object|number} duration - the length of the Interval.\n     * @return {Interval}\n     */\n    static after(start, duration) {\n      const dur = Duration.fromDurationLike(duration),\n        dt = friendlyDateTime(start);\n      return Interval.fromDateTimes(dt, dt.plus(dur));\n    }\n\n    /**\n     * Create an Interval from an end DateTime and a Duration to extend backwards to.\n     * @param {DateTime|Date|Object} end\n     * @param {Duration|Object|number} duration - the length of the Interval.\n     * @return {Interval}\n     */\n    static before(end, duration) {\n      const dur = Duration.fromDurationLike(duration),\n        dt = friendlyDateTime(end);\n      return Interval.fromDateTimes(dt.minus(dur), dt);\n    }\n\n    /**\n     * Create an Interval from an ISO 8601 string.\n     * Accepts `<start>/<end>`, `<start>/<duration>`, and `<duration>/<end>` formats.\n     * @param {string} text - the ISO string to parse\n     * @param {Object} [opts] - options to pass {@link DateTime#fromISO} and optionally {@link Duration#fromISO}\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals\n     * @return {Interval}\n     */\n    static fromISO(text, opts) {\n      const [s, e] = (text || \"\").split(\"/\", 2);\n      if (s && e) {\n        let start, startIsValid;\n        try {\n          start = DateTime.fromISO(s, opts);\n          startIsValid = start.isValid;\n        } catch (e) {\n          startIsValid = false;\n        }\n\n        let end, endIsValid;\n        try {\n          end = DateTime.fromISO(e, opts);\n          endIsValid = end.isValid;\n        } catch (e) {\n          endIsValid = false;\n        }\n\n        if (startIsValid && endIsValid) {\n          return Interval.fromDateTimes(start, end);\n        }\n\n        if (startIsValid) {\n          const dur = Duration.fromISO(e, opts);\n          if (dur.isValid) {\n            return Interval.after(start, dur);\n          }\n        } else if (endIsValid) {\n          const dur = Duration.fromISO(s, opts);\n          if (dur.isValid) {\n            return Interval.before(end, dur);\n          }\n        }\n      }\n      return Interval.invalid(\"unparsable\", `the input \"${text}\" can't be parsed as ISO 8601`);\n    }\n\n    /**\n     * Check if an object is an Interval. Works across context boundaries\n     * @param {object} o\n     * @return {boolean}\n     */\n    static isInterval(o) {\n      return (o && o.isLuxonInterval) || false;\n    }\n\n    /**\n     * Returns the start of the Interval\n     * @type {DateTime}\n     */\n    get start() {\n      return this.isValid ? this.s : null;\n    }\n\n    /**\n     * Returns the end of the Interval\n     * @type {DateTime}\n     */\n    get end() {\n      return this.isValid ? this.e : null;\n    }\n\n    /**\n     * Returns whether this Interval's end is at least its start, meaning that the Interval isn't 'backwards'.\n     * @type {boolean}\n     */\n    get isValid() {\n      return this.invalidReason === null;\n    }\n\n    /**\n     * Returns an error code if this Interval is invalid, or null if the Interval is valid\n     * @type {string}\n     */\n    get invalidReason() {\n      return this.invalid ? this.invalid.reason : null;\n    }\n\n    /**\n     * Returns an explanation of why this Interval became invalid, or null if the Interval is valid\n     * @type {string}\n     */\n    get invalidExplanation() {\n      return this.invalid ? this.invalid.explanation : null;\n    }\n\n    /**\n     * Returns the length of the Interval in the specified unit.\n     * @param {string} unit - the unit (such as 'hours' or 'days') to return the length in.\n     * @return {number}\n     */\n    length(unit = \"milliseconds\") {\n      return this.isValid ? this.toDuration(...[unit]).get(unit) : NaN;\n    }\n\n    /**\n     * Returns the count of minutes, hours, days, months, or years included in the Interval, even in part.\n     * Unlike {@link Interval#length} this counts sections of the calendar, not periods of time, e.g. specifying 'day'\n     * asks 'what dates are included in this interval?', not 'how many days long is this interval?'\n     * @param {string} [unit='milliseconds'] - the unit of time to count.\n     * @param {Object} opts - options\n     * @param {boolean} [opts.useLocaleWeeks=false] - If true, use weeks based on the locale, i.e. use the locale-dependent start of the week; this operation will always use the locale of the start DateTime\n     * @return {number}\n     */\n    count(unit = \"milliseconds\", opts) {\n      if (!this.isValid) return NaN;\n      const start = this.start.startOf(unit, opts);\n      let end;\n      if (opts?.useLocaleWeeks) {\n        end = this.end.reconfigure({ locale: start.locale });\n      } else {\n        end = this.end;\n      }\n      end = end.startOf(unit, opts);\n      return Math.floor(end.diff(start, unit).get(unit)) + (end.valueOf() !== this.end.valueOf());\n    }\n\n    /**\n     * Returns whether this Interval's start and end are both in the same unit of time\n     * @param {string} unit - the unit of time to check sameness on\n     * @return {boolean}\n     */\n    hasSame(unit) {\n      return this.isValid ? this.isEmpty() || this.e.minus(1).hasSame(this.s, unit) : false;\n    }\n\n    /**\n     * Return whether this Interval has the same start and end DateTimes.\n     * @return {boolean}\n     */\n    isEmpty() {\n      return this.s.valueOf() === this.e.valueOf();\n    }\n\n    /**\n     * Return whether this Interval's start is after the specified DateTime.\n     * @param {DateTime} dateTime\n     * @return {boolean}\n     */\n    isAfter(dateTime) {\n      if (!this.isValid) return false;\n      return this.s > dateTime;\n    }\n\n    /**\n     * Return whether this Interval's end is before the specified DateTime.\n     * @param {DateTime} dateTime\n     * @return {boolean}\n     */\n    isBefore(dateTime) {\n      if (!this.isValid) return false;\n      return this.e <= dateTime;\n    }\n\n    /**\n     * Return whether this Interval contains the specified DateTime.\n     * @param {DateTime} dateTime\n     * @return {boolean}\n     */\n    contains(dateTime) {\n      if (!this.isValid) return false;\n      return this.s <= dateTime && this.e > dateTime;\n    }\n\n    /**\n     * \"Sets\" the start and/or end dates. Returns a newly-constructed Interval.\n     * @param {Object} values - the values to set\n     * @param {DateTime} values.start - the starting DateTime\n     * @param {DateTime} values.end - the ending DateTime\n     * @return {Interval}\n     */\n    set({ start, end } = {}) {\n      if (!this.isValid) return this;\n      return Interval.fromDateTimes(start || this.s, end || this.e);\n    }\n\n    /**\n     * Split this Interval at each of the specified DateTimes\n     * @param {...DateTime} dateTimes - the unit of time to count.\n     * @return {Array}\n     */\n    splitAt(...dateTimes) {\n      if (!this.isValid) return [];\n      const sorted = dateTimes\n          .map(friendlyDateTime)\n          .filter((d) => this.contains(d))\n          .sort((a, b) => a.toMillis() - b.toMillis()),\n        results = [];\n      let { s } = this,\n        i = 0;\n\n      while (s < this.e) {\n        const added = sorted[i] || this.e,\n          next = +added > +this.e ? this.e : added;\n        results.push(Interval.fromDateTimes(s, next));\n        s = next;\n        i += 1;\n      }\n\n      return results;\n    }\n\n    /**\n     * Split this Interval into smaller Intervals, each of the specified length.\n     * Left over time is grouped into a smaller interval\n     * @param {Duration|Object|number} duration - The length of each resulting interval.\n     * @return {Array}\n     */\n    splitBy(duration) {\n      const dur = Duration.fromDurationLike(duration);\n\n      if (!this.isValid || !dur.isValid || dur.as(\"milliseconds\") === 0) {\n        return [];\n      }\n\n      let { s } = this,\n        idx = 1,\n        next;\n\n      const results = [];\n      while (s < this.e) {\n        const added = this.start.plus(dur.mapUnits((x) => x * idx));\n        next = +added > +this.e ? this.e : added;\n        results.push(Interval.fromDateTimes(s, next));\n        s = next;\n        idx += 1;\n      }\n\n      return results;\n    }\n\n    /**\n     * Split this Interval into the specified number of smaller intervals.\n     * @param {number} numberOfParts - The number of Intervals to divide the Interval into.\n     * @return {Array}\n     */\n    divideEqually(numberOfParts) {\n      if (!this.isValid) return [];\n      return this.splitBy(this.length() / numberOfParts).slice(0, numberOfParts);\n    }\n\n    /**\n     * Return whether this Interval overlaps with the specified Interval\n     * @param {Interval} other\n     * @return {boolean}\n     */\n    overlaps(other) {\n      return this.e > other.s && this.s < other.e;\n    }\n\n    /**\n     * Return whether this Interval's end is adjacent to the specified Interval's start.\n     * @param {Interval} other\n     * @return {boolean}\n     */\n    abutsStart(other) {\n      if (!this.isValid) return false;\n      return +this.e === +other.s;\n    }\n\n    /**\n     * Return whether this Interval's start is adjacent to the specified Interval's end.\n     * @param {Interval} other\n     * @return {boolean}\n     */\n    abutsEnd(other) {\n      if (!this.isValid) return false;\n      return +other.e === +this.s;\n    }\n\n    /**\n     * Return whether this Interval engulfs the start and end of the specified Interval.\n     * @param {Interval} other\n     * @return {boolean}\n     */\n    engulfs(other) {\n      if (!this.isValid) return false;\n      return this.s <= other.s && this.e >= other.e;\n    }\n\n    /**\n     * Return whether this Interval has the same start and end as the specified Interval.\n     * @param {Interval} other\n     * @return {boolean}\n     */\n    equals(other) {\n      if (!this.isValid || !other.isValid) {\n        return false;\n      }\n\n      return this.s.equals(other.s) && this.e.equals(other.e);\n    }\n\n    /**\n     * Return an Interval representing the intersection of this Interval and the specified Interval.\n     * Specifically, the resulting Interval has the maximum start time and the minimum end time of the two Intervals.\n     * Returns null if the intersection is empty, meaning, the intervals don't intersect.\n     * @param {Interval} other\n     * @return {Interval}\n     */\n    intersection(other) {\n      if (!this.isValid) return this;\n      const s = this.s > other.s ? this.s : other.s,\n        e = this.e < other.e ? this.e : other.e;\n\n      if (s >= e) {\n        return null;\n      } else {\n        return Interval.fromDateTimes(s, e);\n      }\n    }\n\n    /**\n     * Return an Interval representing the union of this Interval and the specified Interval.\n     * Specifically, the resulting Interval has the minimum start time and the maximum end time of the two Intervals.\n     * @param {Interval} other\n     * @return {Interval}\n     */\n    union(other) {\n      if (!this.isValid) return this;\n      const s = this.s < other.s ? this.s : other.s,\n        e = this.e > other.e ? this.e : other.e;\n      return Interval.fromDateTimes(s, e);\n    }\n\n    /**\n     * Merge an array of Intervals into a equivalent minimal set of Intervals.\n     * Combines overlapping and adjacent Intervals.\n     * @param {Array} intervals\n     * @return {Array}\n     */\n    static merge(intervals) {\n      const [found, final] = intervals\n        .sort((a, b) => a.s - b.s)\n        .reduce(\n          ([sofar, current], item) => {\n            if (!current) {\n              return [sofar, item];\n            } else if (current.overlaps(item) || current.abutsStart(item)) {\n              return [sofar, current.union(item)];\n            } else {\n              return [sofar.concat([current]), item];\n            }\n          },\n          [[], null]\n        );\n      if (final) {\n        found.push(final);\n      }\n      return found;\n    }\n\n    /**\n     * Return an array of Intervals representing the spans of time that only appear in one of the specified Intervals.\n     * @param {Array} intervals\n     * @return {Array}\n     */\n    static xor(intervals) {\n      let start = null,\n        currentCount = 0;\n      const results = [],\n        ends = intervals.map((i) => [\n          { time: i.s, type: \"s\" },\n          { time: i.e, type: \"e\" },\n        ]),\n        flattened = Array.prototype.concat(...ends),\n        arr = flattened.sort((a, b) => a.time - b.time);\n\n      for (const i of arr) {\n        currentCount += i.type === \"s\" ? 1 : -1;\n\n        if (currentCount === 1) {\n          start = i.time;\n        } else {\n          if (start && +start !== +i.time) {\n            results.push(Interval.fromDateTimes(start, i.time));\n          }\n\n          start = null;\n        }\n      }\n\n      return Interval.merge(results);\n    }\n\n    /**\n     * Return an Interval representing the span of time in this Interval that doesn't overlap with any of the specified Intervals.\n     * @param {...Interval} intervals\n     * @return {Array}\n     */\n    difference(...intervals) {\n      return Interval.xor([this].concat(intervals))\n        .map((i) => this.intersection(i))\n        .filter((i) => i && !i.isEmpty());\n    }\n\n    /**\n     * Returns a string representation of this Interval appropriate for debugging.\n     * @return {string}\n     */\n    toString() {\n      if (!this.isValid) return INVALID$1;\n      return `[${this.s.toISO()} \u2013 ${this.e.toISO()})`;\n    }\n\n    /**\n     * Returns a string representation of this Interval appropriate for the REPL.\n     * @return {string}\n     */\n    [Symbol.for(\"nodejs.util.inspect.custom\")]() {\n      if (this.isValid) {\n        return `Interval { start: ${this.s.toISO()}, end: ${this.e.toISO()} }`;\n      } else {\n        return `Interval { Invalid, reason: ${this.invalidReason} }`;\n      }\n    }\n\n    /**\n     * Returns a localized string representing this Interval. Accepts the same options as the\n     * Intl.DateTimeFormat constructor and any presets defined by Luxon, such as\n     * {@link DateTime.DATE_FULL} or {@link DateTime.TIME_SIMPLE}. The exact behavior of this method\n     * is browser-specific, but in general it will return an appropriate representation of the\n     * Interval in the assigned locale. Defaults to the system's locale if no locale has been\n     * specified.\n     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat\n     * @param {Object} [formatOpts=DateTime.DATE_SHORT] - Either a DateTime preset or\n     * Intl.DateTimeFormat constructor options.\n     * @param {Object} opts - Options to override the configuration of the start DateTime.\n     * @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(); //=> 11/7/2022 \u2013 11/8/2022\n     * @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(DateTime.DATE_FULL); //=> November 7 \u2013 8, 2022\n     * @example Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(DateTime.DATE_FULL, { locale: 'fr-FR' }); //=> 7\u20138 novembre 2022\n     * @example Interval.fromISO('2022-11-07T17:00Z/2022-11-07T19:00Z').toLocaleString(DateTime.TIME_SIMPLE); //=> 6:00 \u2013 8:00 PM\n     * @example Interval.fromISO('2022-11-07T17:00Z/2022-11-07T19:00Z').toLocaleString({ weekday: 'short', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); //=> Mon, Nov 07, 6:00 \u2013 8:00 p\n     * @return {string}\n     */\n    toLocaleString(formatOpts = DATE_SHORT, opts = {}) {\n      return this.isValid\n        ? Formatter.create(this.s.loc.clone(opts), formatOpts).formatInterval(this)\n        : INVALID$1;\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of this Interval.\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals\n     * @param {Object} opts - The same options as {@link DateTime#toISO}\n     * @return {string}\n     */\n    toISO(opts) {\n      if (!this.isValid) return INVALID$1;\n      return `${this.s.toISO(opts)}/${this.e.toISO(opts)}`;\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of date of this Interval.\n     * The time components are ignored.\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals\n     * @return {string}\n     */\n    toISODate() {\n      if (!this.isValid) return INVALID$1;\n      return `${this.s.toISODate()}/${this.e.toISODate()}`;\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of time of this Interval.\n     * The date components are ignored.\n     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals\n     * @param {Object} opts - The same options as {@link DateTime#toISO}\n     * @return {string}\n     */\n    toISOTime(opts) {\n      if (!this.isValid) return INVALID$1;\n      return `${this.s.toISOTime(opts)}/${this.e.toISOTime(opts)}`;\n    }\n\n    /**\n     * Returns a string representation of this Interval formatted according to the specified format\n     * string. **You may not want this.** See {@link Interval#toLocaleString} for a more flexible\n     * formatting tool.\n     * @param {string} dateFormat - The format string. This string formats the start and end time.\n     * See {@link DateTime#toFormat} for details.\n     * @param {Object} opts - Options.\n     * @param {string} [opts.separator =  ' \u2013 '] - A separator to place between the start and end\n     * representations.\n     * @return {string}\n     */\n    toFormat(dateFormat, { separator = \" \u2013 \" } = {}) {\n      if (!this.isValid) return INVALID$1;\n      return `${this.s.toFormat(dateFormat)}${separator}${this.e.toFormat(dateFormat)}`;\n    }\n\n    /**\n     * Return a Duration representing the time spanned by this interval.\n     * @param {string|string[]} [unit=['milliseconds']] - the unit or units (such as 'hours' or 'days') to include in the duration.\n     * @param {Object} opts - options that affect the creation of the Duration\n     * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use\n     * @example Interval.fromDateTimes(dt1, dt2).toDuration().toObject() //=> { milliseconds: 88489257 }\n     * @example Interval.fromDateTimes(dt1, dt2).toDuration('days').toObject() //=> { days: 1.0241812152777778 }\n     * @example Interval.fromDateTimes(dt1, dt2).toDuration(['hours', 'minutes']).toObject() //=> { hours: 24, minutes: 34.82095 }\n     * @example Interval.fromDateTimes(dt1, dt2).toDuration(['hours', 'minutes', 'seconds']).toObject() //=> { hours: 24, minutes: 34, seconds: 49.257 }\n     * @example Interval.fromDateTimes(dt1, dt2).toDuration('seconds').toObject() //=> { seconds: 88489.257 }\n     * @return {Duration}\n     */\n    toDuration(unit, opts) {\n      if (!this.isValid) {\n        return Duration.invalid(this.invalidReason);\n      }\n      return this.e.diff(this.s, unit, opts);\n    }\n\n    /**\n     * Run mapFn on the interval start and end, returning a new Interval from the resulting DateTimes\n     * @param {function} mapFn\n     * @return {Interval}\n     * @example Interval.fromDateTimes(dt1, dt2).mapEndpoints(endpoint => endpoint.toUTC())\n     * @example Interval.fromDateTimes(dt1, dt2).mapEndpoints(endpoint => endpoint.plus({ hours: 2 }))\n     */\n    mapEndpoints(mapFn) {\n      return Interval.fromDateTimes(mapFn(this.s), mapFn(this.e));\n    }\n  }\n\n  /**\n   * The Info class contains static methods for retrieving general time and date related data. For example, it has methods for finding out if a time zone has a DST, for listing the months in any supported locale, and for discovering which of Luxon features are available in the current environment.\n   */\n  class Info {\n    /**\n     * Return whether the specified zone contains a DST.\n     * @param {string|Zone} [zone='local'] - Zone to check. Defaults to the environment's local zone.\n     * @return {boolean}\n     */\n    static hasDST(zone = Settings.defaultZone) {\n      const proto = DateTime.now().setZone(zone).set({ month: 12 });\n\n      return !zone.isUniversal && proto.offset !== proto.set({ month: 6 }).offset;\n    }\n\n    /**\n     * Return whether the specified zone is a valid IANA specifier.\n     * @param {string} zone - Zone to check\n     * @return {boolean}\n     */\n    static isValidIANAZone(zone) {\n      return IANAZone.isValidZone(zone);\n    }\n\n    /**\n     * Converts the input into a {@link Zone} instance.\n     *\n     * * If `input` is already a Zone instance, it is returned unchanged.\n     * * If `input` is a string containing a valid time zone name, a Zone instance\n     *   with that name is returned.\n     * * If `input` is a string that doesn't refer to a known time zone, a Zone\n     *   instance with {@link Zone#isValid} == false is returned.\n     * * If `input is a number, a Zone instance with the specified fixed offset\n     *   in minutes is returned.\n     * * If `input` is `null` or `undefined`, the default zone is returned.\n     * @param {string|Zone|number} [input] - the value to be converted\n     * @return {Zone}\n     */\n    static normalizeZone(input) {\n      return normalizeZone(input, Settings.defaultZone);\n    }\n\n    /**\n     * Get the weekday on which the week starts according to the given locale.\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @param {string} [opts.locObj=null] - an existing locale object to use\n     * @returns {number} the start of the week, 1 for Monday through 7 for Sunday\n     */\n    static getStartOfWeek({ locale = null, locObj = null } = {}) {\n      return (locObj || Locale.create(locale)).getStartOfWeek();\n    }\n\n    /**\n     * Get the minimum number of days necessary in a week before it is considered part of the next year according\n     * to the given locale.\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @param {string} [opts.locObj=null] - an existing locale object to use\n     * @returns {number}\n     */\n    static getMinimumDaysInFirstWeek({ locale = null, locObj = null } = {}) {\n      return (locObj || Locale.create(locale)).getMinDaysInFirstWeek();\n    }\n\n    /**\n     * Get the weekdays, which are considered the weekend according to the given locale\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @param {string} [opts.locObj=null] - an existing locale object to use\n     * @returns {number[]} an array of weekdays, 1 for Monday through 7 for Sunday\n     */\n    static getWeekendWeekdays({ locale = null, locObj = null } = {}) {\n      // copy the array, because we cache it internally\n      return (locObj || Locale.create(locale)).getWeekendDays().slice();\n    }\n\n    /**\n     * Return an array of standalone month names.\n     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat\n     * @param {string} [length='long'] - the length of the month representation, such as \"numeric\", \"2-digit\", \"narrow\", \"short\", \"long\"\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @param {string} [opts.numberingSystem=null] - the numbering system\n     * @param {string} [opts.locObj=null] - an existing locale object to use\n     * @param {string} [opts.outputCalendar='gregory'] - the calendar\n     * @example Info.months()[0] //=> 'January'\n     * @example Info.months('short')[0] //=> 'Jan'\n     * @example Info.months('numeric')[0] //=> '1'\n     * @example Info.months('short', { locale: 'fr-CA' } )[0] //=> 'janv.'\n     * @example Info.months('numeric', { locale: 'ar' })[0] //=> '\u0661'\n     * @example Info.months('long', { outputCalendar: 'islamic' })[0] //=> 'Rabi\u02bb I'\n     * @return {Array}\n     */\n    static months(\n      length = \"long\",\n      { locale = null, numberingSystem = null, locObj = null, outputCalendar = \"gregory\" } = {}\n    ) {\n      return (locObj || Locale.create(locale, numberingSystem, outputCalendar)).months(length);\n    }\n\n    /**\n     * Return an array of format month names.\n     * Format months differ from standalone months in that they're meant to appear next to the day of the month. In some languages, that\n     * changes the string.\n     * See {@link Info#months}\n     * @param {string} [length='long'] - the length of the month representation, such as \"numeric\", \"2-digit\", \"narrow\", \"short\", \"long\"\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @param {string} [opts.numberingSystem=null] - the numbering system\n     * @param {string} [opts.locObj=null] - an existing locale object to use\n     * @param {string} [opts.outputCalendar='gregory'] - the calendar\n     * @return {Array}\n     */\n    static monthsFormat(\n      length = \"long\",\n      { locale = null, numberingSystem = null, locObj = null, outputCalendar = \"gregory\" } = {}\n    ) {\n      return (locObj || Locale.create(locale, numberingSystem, outputCalendar)).months(length, true);\n    }\n\n    /**\n     * Return an array of standalone week names.\n     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat\n     * @param {string} [length='long'] - the length of the weekday representation, such as \"narrow\", \"short\", \"long\".\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @param {string} [opts.numberingSystem=null] - the numbering system\n     * @param {string} [opts.locObj=null] - an existing locale object to use\n     * @example Info.weekdays()[0] //=> 'Monday'\n     * @example Info.weekdays('short')[0] //=> 'Mon'\n     * @example Info.weekdays('short', { locale: 'fr-CA' })[0] //=> 'lun.'\n     * @example Info.weekdays('short', { locale: 'ar' })[0] //=> '\u0627\u0644\u0627\u062b\u0646\u064a\u0646'\n     * @return {Array}\n     */\n    static weekdays(length = \"long\", { locale = null, numberingSystem = null, locObj = null } = {}) {\n      return (locObj || Locale.create(locale, numberingSystem, null)).weekdays(length);\n    }\n\n    /**\n     * Return an array of format week names.\n     * Format weekdays differ from standalone weekdays in that they're meant to appear next to more date information. In some languages, that\n     * changes the string.\n     * See {@link Info#weekdays}\n     * @param {string} [length='long'] - the length of the month representation, such as \"narrow\", \"short\", \"long\".\n     * @param {Object} opts - options\n     * @param {string} [opts.locale=null] - the locale code\n     * @param {string} [opts.numberingSystem=null] - the numbering system\n     * @param {string} [opts.locObj=null] - an existing locale object to use\n     * @return {Array}\n     */\n    static weekdaysFormat(\n      length = \"long\",\n      { locale = null, numberingSystem = null, locObj = null } = {}\n    ) {\n      return (locObj || Locale.create(locale, numberingSystem, null)).weekdays(length, true);\n    }\n\n    /**\n     * Return an array of meridiems.\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @example Info.meridiems() //=> [ 'AM', 'PM' ]\n     * @example Info.meridiems({ locale: 'my' }) //=> [ '\u1014\u1036\u1014\u1000\u103a', '\u100a\u1014\u1031' ]\n     * @return {Array}\n     */\n    static meridiems({ locale = null } = {}) {\n      return Locale.create(locale).meridiems();\n    }\n\n    /**\n     * Return an array of eras, such as ['BC', 'AD']. The locale can be specified, but the calendar system is always Gregorian.\n     * @param {string} [length='short'] - the length of the era representation, such as \"short\" or \"long\".\n     * @param {Object} opts - options\n     * @param {string} [opts.locale] - the locale code\n     * @example Info.eras() //=> [ 'BC', 'AD' ]\n     * @example Info.eras('long') //=> [ 'Before Christ', 'Anno Domini' ]\n     * @example Info.eras('long', { locale: 'fr' }) //=> [ 'avant J\u00e9sus-Christ', 'apr\u00e8s J\u00e9sus-Christ' ]\n     * @return {Array}\n     */\n    static eras(length = \"short\", { locale = null } = {}) {\n      return Locale.create(locale, null, \"gregory\").eras(length);\n    }\n\n    /**\n     * Return the set of available features in this environment.\n     * Some features of Luxon are not available in all environments. For example, on older browsers, relative time formatting support is not available. Use this function to figure out if that's the case.\n     * Keys:\n     * * `relative`: whether this environment supports relative time formatting\n     * * `localeWeek`: whether this environment supports different weekdays for the start of the week based on the locale\n     * @example Info.features() //=> { relative: false, localeWeek: true }\n     * @return {Object}\n     */\n    static features() {\n      return { relative: hasRelative(), localeWeek: hasLocaleWeekInfo() };\n    }\n  }\n\n  function dayDiff(earlier, later) {\n    const utcDayStart = (dt) => dt.toUTC(0, { keepLocalTime: true }).startOf(\"day\").valueOf(),\n      ms = utcDayStart(later) - utcDayStart(earlier);\n    return Math.floor(Duration.fromMillis(ms).as(\"days\"));\n  }\n\n  function highOrderDiffs(cursor, later, units) {\n    const differs = [\n      [\"years\", (a, b) => b.year - a.year],\n      [\"quarters\", (a, b) => b.quarter - a.quarter + (b.year - a.year) * 4],\n      [\"months\", (a, b) => b.month - a.month + (b.year - a.year) * 12],\n      [\n        \"weeks\",\n        (a, b) => {\n          const days = dayDiff(a, b);\n          return (days - (days % 7)) / 7;\n        },\n      ],\n      [\"days\", dayDiff],\n    ];\n\n    const results = {};\n    const earlier = cursor;\n    let lowestOrder, highWater;\n\n    /* This loop tries to diff using larger units first.\n       If we overshoot, we backtrack and try the next smaller unit.\n       \"cursor\" starts out at the earlier timestamp and moves closer and closer to \"later\"\n       as we use smaller and smaller units.\n       highWater keeps track of where we would be if we added one more of the smallest unit,\n       this is used later to potentially convert any difference smaller than the smallest higher order unit\n       into a fraction of that smallest higher order unit\n    */\n    for (const [unit, differ] of differs) {\n      if (units.indexOf(unit) >= 0) {\n        lowestOrder = unit;\n\n        results[unit] = differ(cursor, later);\n        highWater = earlier.plus(results);\n\n        if (highWater > later) {\n          // we overshot the end point, backtrack cursor by 1\n          results[unit]--;\n          cursor = earlier.plus(results);\n\n          // if we are still overshooting now, we need to backtrack again\n          // this happens in certain situations when diffing times in different zones,\n          // because this calculation ignores time zones\n          if (cursor > later) {\n            // keep the \"overshot by 1\" around as highWater\n            highWater = cursor;\n            // backtrack cursor by 1\n            results[unit]--;\n            cursor = earlier.plus(results);\n          }\n        } else {\n          cursor = highWater;\n        }\n      }\n    }\n\n    return [cursor, results, highWater, lowestOrder];\n  }\n\n  function diff (earlier, later, units, opts) {\n    let [cursor, results, highWater, lowestOrder] = highOrderDiffs(earlier, later, units);\n\n    const remainingMillis = later - cursor;\n\n    const lowerOrderUnits = units.filter(\n      (u) => [\"hours\", \"minutes\", \"seconds\", \"milliseconds\"].indexOf(u) >= 0\n    );\n\n    if (lowerOrderUnits.length === 0) {\n      if (highWater < later) {\n        highWater = cursor.plus({ [lowestOrder]: 1 });\n      }\n\n      if (highWater !== cursor) {\n        results[lowestOrder] = (results[lowestOrder] || 0) + remainingMillis / (highWater - cursor);\n      }\n    }\n\n    const duration = Duration.fromObject(results, opts);\n\n    if (lowerOrderUnits.length > 0) {\n      return Duration.fromMillis(remainingMillis, opts)\n        .shiftTo(...lowerOrderUnits)\n        .plus(duration);\n    } else {\n      return duration;\n    }\n  }\n\n  const numberingSystems = {\n    arab: \"[\\u0660-\\u0669]\",\n    arabext: \"[\\u06F0-\\u06F9]\",\n    bali: \"[\\u1B50-\\u1B59]\",\n    beng: \"[\\u09E6-\\u09EF]\",\n    deva: \"[\\u0966-\\u096F]\",\n    fullwide: \"[\\uFF10-\\uFF19]\",\n    gujr: \"[\\u0AE6-\\u0AEF]\",\n    hanidec: \"[\u3007|\u4e00|\u4e8c|\u4e09|\u56db|\u4e94|\u516d|\u4e03|\u516b|\u4e5d]\",\n    khmr: \"[\\u17E0-\\u17E9]\",\n    knda: \"[\\u0CE6-\\u0CEF]\",\n    laoo: \"[\\u0ED0-\\u0ED9]\",\n    limb: \"[\\u1946-\\u194F]\",\n    mlym: \"[\\u0D66-\\u0D6F]\",\n    mong: \"[\\u1810-\\u1819]\",\n    mymr: \"[\\u1040-\\u1049]\",\n    orya: \"[\\u0B66-\\u0B6F]\",\n    tamldec: \"[\\u0BE6-\\u0BEF]\",\n    telu: \"[\\u0C66-\\u0C6F]\",\n    thai: \"[\\u0E50-\\u0E59]\",\n    tibt: \"[\\u0F20-\\u0F29]\",\n    latn: \"\\\\d\",\n  };\n\n  const numberingSystemsUTF16 = {\n    arab: [1632, 1641],\n    arabext: [1776, 1785],\n    bali: [6992, 7001],\n    beng: [2534, 2543],\n    deva: [2406, 2415],\n    fullwide: [65296, 65303],\n    gujr: [2790, 2799],\n    khmr: [6112, 6121],\n    knda: [3302, 3311],\n    laoo: [3792, 3801],\n    limb: [6470, 6479],\n    mlym: [3430, 3439],\n    mong: [6160, 6169],\n    mymr: [4160, 4169],\n    orya: [2918, 2927],\n    tamldec: [3046, 3055],\n    telu: [3174, 3183],\n    thai: [3664, 3673],\n    tibt: [3872, 3881],\n  };\n\n  const hanidecChars = numberingSystems.hanidec.replace(/[\\[|\\]]/g, \"\").split(\"\");\n\n  function parseDigits(str) {\n    let value = parseInt(str, 10);\n    if (isNaN(value)) {\n      value = \"\";\n      for (let i = 0; i < str.length; i++) {\n        const code = str.charCodeAt(i);\n\n        if (str[i].search(numberingSystems.hanidec) !== -1) {\n          value += hanidecChars.indexOf(str[i]);\n        } else {\n          for (const key in numberingSystemsUTF16) {\n            const [min, max] = numberingSystemsUTF16[key];\n            if (code >= min && code <= max) {\n              value += code - min;\n            }\n          }\n        }\n      }\n      return parseInt(value, 10);\n    } else {\n      return value;\n    }\n  }\n\n  function digitRegex({ numberingSystem }, append = \"\") {\n    return new RegExp(`${numberingSystems[numberingSystem || \"latn\"]}${append}`);\n  }\n\n  const MISSING_FTP = \"missing Intl.DateTimeFormat.formatToParts support\";\n\n  function intUnit(regex, post = (i) => i) {\n    return { regex, deser: ([s]) => post(parseDigits(s)) };\n  }\n\n  const NBSP = String.fromCharCode(160);\n  const spaceOrNBSP = `[ ${NBSP}]`;\n  const spaceOrNBSPRegExp = new RegExp(spaceOrNBSP, \"g\");\n\n  function fixListRegex(s) {\n    // make dots optional and also make them literal\n    // make space and non breakable space characters interchangeable\n    return s.replace(/\\./g, \"\\\\.?\").replace(spaceOrNBSPRegExp, spaceOrNBSP);\n  }\n\n  function stripInsensitivities(s) {\n    return s\n      .replace(/\\./g, \"\") // ignore dots that were made optional\n      .replace(spaceOrNBSPRegExp, \" \") // interchange space and nbsp\n      .toLowerCase();\n  }\n\n  function oneOf(strings, startIndex) {\n    if (strings === null) {\n      return null;\n    } else {\n      return {\n        regex: RegExp(strings.map(fixListRegex).join(\"|\")),\n        deser: ([s]) =>\n          strings.findIndex((i) => stripInsensitivities(s) === stripInsensitivities(i)) + startIndex,\n      };\n    }\n  }\n\n  function offset(regex, groups) {\n    return { regex, deser: ([, h, m]) => signedOffset(h, m), groups };\n  }\n\n  function simple(regex) {\n    return { regex, deser: ([s]) => s };\n  }\n\n  function escapeToken(value) {\n    return value.replace(/[\\-\\[\\]{}()*+?.,\\\\\\^$|#\\s]/g, \"\\\\$&\");\n  }\n\n  /**\n   * @param token\n   * @param {Locale} loc\n   */\n  function unitForToken(token, loc) {\n    const one = digitRegex(loc),\n      two = digitRegex(loc, \"{2}\"),\n      three = digitRegex(loc, \"{3}\"),\n      four = digitRegex(loc, \"{4}\"),\n      six = digitRegex(loc, \"{6}\"),\n      oneOrTwo = digitRegex(loc, \"{1,2}\"),\n      oneToThree = digitRegex(loc, \"{1,3}\"),\n      oneToSix = digitRegex(loc, \"{1,6}\"),\n      oneToNine = digitRegex(loc, \"{1,9}\"),\n      twoToFour = digitRegex(loc, \"{2,4}\"),\n      fourToSix = digitRegex(loc, \"{4,6}\"),\n      literal = (t) => ({ regex: RegExp(escapeToken(t.val)), deser: ([s]) => s, literal: true }),\n      unitate = (t) => {\n        if (token.literal) {\n          return literal(t);\n        }\n        switch (t.val) {\n          // era\n          case \"G\":\n            return oneOf(loc.eras(\"short\"), 0);\n          case \"GG\":\n            return oneOf(loc.eras(\"long\"), 0);\n          // years\n          case \"y\":\n            return intUnit(oneToSix);\n          case \"yy\":\n            return intUnit(twoToFour, untruncateYear);\n          case \"yyyy\":\n            return intUnit(four);\n          case \"yyyyy\":\n            return intUnit(fourToSix);\n          case \"yyyyyy\":\n            return intUnit(six);\n          // months\n          case \"M\":\n            return intUnit(oneOrTwo);\n          case \"MM\":\n            return intUnit(two);\n          case \"MMM\":\n            return oneOf(loc.months(\"short\", true), 1);\n          case \"MMMM\":\n            return oneOf(loc.months(\"long\", true), 1);\n          case \"L\":\n            return intUnit(oneOrTwo);\n          case \"LL\":\n            return intUnit(two);\n          case \"LLL\":\n            return oneOf(loc.months(\"short\", false), 1);\n          case \"LLLL\":\n            return oneOf(loc.months(\"long\", false), 1);\n          // dates\n          case \"d\":\n            return intUnit(oneOrTwo);\n          case \"dd\":\n            return intUnit(two);\n          // ordinals\n          case \"o\":\n            return intUnit(oneToThree);\n          case \"ooo\":\n            return intUnit(three);\n          // time\n          case \"HH\":\n            return intUnit(two);\n          case \"H\":\n            return intUnit(oneOrTwo);\n          case \"hh\":\n            return intUnit(two);\n          case \"h\":\n            return intUnit(oneOrTwo);\n          case \"mm\":\n            return intUnit(two);\n          case \"m\":\n            return intUnit(oneOrTwo);\n          case \"q\":\n            return intUnit(oneOrTwo);\n          case \"qq\":\n            return intUnit(two);\n          case \"s\":\n            return intUnit(oneOrTwo);\n          case \"ss\":\n            return intUnit(two);\n          case \"S\":\n            return intUnit(oneToThree);\n          case \"SSS\":\n            return intUnit(three);\n          case \"u\":\n            return simple(oneToNine);\n          case \"uu\":\n            return simple(oneOrTwo);\n          case \"uuu\":\n            return intUnit(one);\n          // meridiem\n          case \"a\":\n            return oneOf(loc.meridiems(), 0);\n          // weekYear (k)\n          case \"kkkk\":\n            return intUnit(four);\n          case \"kk\":\n            return intUnit(twoToFour, untruncateYear);\n          // weekNumber (W)\n          case \"W\":\n            return intUnit(oneOrTwo);\n          case \"WW\":\n            return intUnit(two);\n          // weekdays\n          case \"E\":\n          case \"c\":\n            return intUnit(one);\n          case \"EEE\":\n            return oneOf(loc.weekdays(\"short\", false), 1);\n          case \"EEEE\":\n            return oneOf(loc.weekdays(\"long\", false), 1);\n          case \"ccc\":\n            return oneOf(loc.weekdays(\"short\", true), 1);\n          case \"cccc\":\n            return oneOf(loc.weekdays(\"long\", true), 1);\n          // offset/zone\n          case \"Z\":\n          case \"ZZ\":\n            return offset(new RegExp(`([+-]${oneOrTwo.source})(?::(${two.source}))?`), 2);\n          case \"ZZZ\":\n            return offset(new RegExp(`([+-]${oneOrTwo.source})(${two.source})?`), 2);\n          // we don't support ZZZZ (PST) or ZZZZZ (Pacific Standard Time) in parsing\n          // because we don't have any way to figure out what they are\n          case \"z\":\n            return simple(/[a-z_+-/]{1,256}?/i);\n          // this special-case \"token\" represents a place where a macro-token expanded into a white-space literal\n          // in this case we accept any non-newline white-space\n          case \" \":\n            return simple(/[^\\S\\n\\r]/);\n          default:\n            return literal(t);\n        }\n      };\n\n    const unit = unitate(token) || {\n      invalidReason: MISSING_FTP,\n    };\n\n    unit.token = token;\n\n    return unit;\n  }\n\n  const partTypeStyleToTokenVal = {\n    year: {\n      \"2-digit\": \"yy\",\n      numeric: \"yyyyy\",\n    },\n    month: {\n      numeric: \"M\",\n      \"2-digit\": \"MM\",\n      short: \"MMM\",\n      long: \"MMMM\",\n    },\n    day: {\n      numeric: \"d\",\n      \"2-digit\": \"dd\",\n    },\n    weekday: {\n      short: \"EEE\",\n      long: \"EEEE\",\n    },\n    dayperiod: \"a\",\n    dayPeriod: \"a\",\n    hour12: {\n      numeric: \"h\",\n      \"2-digit\": \"hh\",\n    },\n    hour24: {\n      numeric: \"H\",\n      \"2-digit\": \"HH\",\n    },\n    minute: {\n      numeric: \"m\",\n      \"2-digit\": \"mm\",\n    },\n    second: {\n      numeric: \"s\",\n      \"2-digit\": \"ss\",\n    },\n    timeZoneName: {\n      long: \"ZZZZZ\",\n      short: \"ZZZ\",\n    },\n  };\n\n  function tokenForPart(part, formatOpts, resolvedOpts) {\n    const { type, value } = part;\n\n    if (type === \"literal\") {\n      const isSpace = /^\\s+$/.test(value);\n      return {\n        literal: !isSpace,\n        val: isSpace ? \" \" : value,\n      };\n    }\n\n    const style = formatOpts[type];\n\n    // The user might have explicitly specified hour12 or hourCycle\n    // if so, respect their decision\n    // if not, refer back to the resolvedOpts, which are based on the locale\n    let actualType = type;\n    if (type === \"hour\") {\n      if (formatOpts.hour12 != null) {\n        actualType = formatOpts.hour12 ? \"hour12\" : \"hour24\";\n      } else if (formatOpts.hourCycle != null) {\n        if (formatOpts.hourCycle === \"h11\" || formatOpts.hourCycle === \"h12\") {\n          actualType = \"hour12\";\n        } else {\n          actualType = \"hour24\";\n        }\n      } else {\n        // tokens only differentiate between 24 hours or not,\n        // so we do not need to check hourCycle here, which is less supported anyways\n        actualType = resolvedOpts.hour12 ? \"hour12\" : \"hour24\";\n      }\n    }\n    let val = partTypeStyleToTokenVal[actualType];\n    if (typeof val === \"object\") {\n      val = val[style];\n    }\n\n    if (val) {\n      return {\n        literal: false,\n        val,\n      };\n    }\n\n    return undefined;\n  }\n\n  function buildRegex(units) {\n    const re = units.map((u) => u.regex).reduce((f, r) => `${f}(${r.source})`, \"\");\n    return [`^${re}$`, units];\n  }\n\n  function match(input, regex, handlers) {\n    const matches = input.match(regex);\n\n    if (matches) {\n      const all = {};\n      let matchIndex = 1;\n      for (const i in handlers) {\n        if (hasOwnProperty(handlers, i)) {\n          const h = handlers[i],\n            groups = h.groups ? h.groups + 1 : 1;\n          if (!h.literal && h.token) {\n            all[h.token.val[0]] = h.deser(matches.slice(matchIndex, matchIndex + groups));\n          }\n          matchIndex += groups;\n        }\n      }\n      return [matches, all];\n    } else {\n      return [matches, {}];\n    }\n  }\n\n  function dateTimeFromMatches(matches) {\n    const toField = (token) => {\n      switch (token) {\n        case \"S\":\n          return \"millisecond\";\n        case \"s\":\n          return \"second\";\n        case \"m\":\n          return \"minute\";\n        case \"h\":\n        case \"H\":\n          return \"hour\";\n        case \"d\":\n          return \"day\";\n        case \"o\":\n          return \"ordinal\";\n        case \"L\":\n        case \"M\":\n          return \"month\";\n        case \"y\":\n          return \"year\";\n        case \"E\":\n        case \"c\":\n          return \"weekday\";\n        case \"W\":\n          return \"weekNumber\";\n        case \"k\":\n          return \"weekYear\";\n        case \"q\":\n          return \"quarter\";\n        default:\n          return null;\n      }\n    };\n\n    let zone = null;\n    let specificOffset;\n    if (!isUndefined(matches.z)) {\n      zone = IANAZone.create(matches.z);\n    }\n\n    if (!isUndefined(matches.Z)) {\n      if (!zone) {\n        zone = new FixedOffsetZone(matches.Z);\n      }\n      specificOffset = matches.Z;\n    }\n\n    if (!isUndefined(matches.q)) {\n      matches.M = (matches.q - 1) * 3 + 1;\n    }\n\n    if (!isUndefined(matches.h)) {\n      if (matches.h < 12 && matches.a === 1) {\n        matches.h += 12;\n      } else if (matches.h === 12 && matches.a === 0) {\n        matches.h = 0;\n      }\n    }\n\n    if (matches.G === 0 && matches.y) {\n      matches.y = -matches.y;\n    }\n\n    if (!isUndefined(matches.u)) {\n      matches.S = parseMillis(matches.u);\n    }\n\n    const vals = Object.keys(matches).reduce((r, k) => {\n      const f = toField(k);\n      if (f) {\n        r[f] = matches[k];\n      }\n\n      return r;\n    }, {});\n\n    return [vals, zone, specificOffset];\n  }\n\n  let dummyDateTimeCache = null;\n\n  function getDummyDateTime() {\n    if (!dummyDateTimeCache) {\n      dummyDateTimeCache = DateTime.fromMillis(1555555555555);\n    }\n\n    return dummyDateTimeCache;\n  }\n\n  function maybeExpandMacroToken(token, locale) {\n    if (token.literal) {\n      return token;\n    }\n\n    const formatOpts = Formatter.macroTokenToFormatOpts(token.val);\n    const tokens = formatOptsToTokens(formatOpts, locale);\n\n    if (tokens == null || tokens.includes(undefined)) {\n      return token;\n    }\n\n    return tokens;\n  }\n\n  function expandMacroTokens(tokens, locale) {\n    return Array.prototype.concat(...tokens.map((t) => maybeExpandMacroToken(t, locale)));\n  }\n\n  /**\n   * @private\n   */\n\n  function explainFromTokens(locale, input, format) {\n    const tokens = expandMacroTokens(Formatter.parseFormat(format), locale),\n      units = tokens.map((t) => unitForToken(t, locale)),\n      disqualifyingUnit = units.find((t) => t.invalidReason);\n\n    if (disqualifyingUnit) {\n      return { input, tokens, invalidReason: disqualifyingUnit.invalidReason };\n    } else {\n      const [regexString, handlers] = buildRegex(units),\n        regex = RegExp(regexString, \"i\"),\n        [rawMatches, matches] = match(input, regex, handlers),\n        [result, zone, specificOffset] = matches\n          ? dateTimeFromMatches(matches)\n          : [null, null, undefined];\n      if (hasOwnProperty(matches, \"a\") && hasOwnProperty(matches, \"H\")) {\n        throw new ConflictingSpecificationError(\n          \"Can't include meridiem when specifying 24-hour format\"\n        );\n      }\n      return { input, tokens, regex, rawMatches, matches, result, zone, specificOffset };\n    }\n  }\n\n  function parseFromTokens(locale, input, format) {\n    const { result, zone, specificOffset, invalidReason } = explainFromTokens(locale, input, format);\n    return [result, zone, specificOffset, invalidReason];\n  }\n\n  function formatOptsToTokens(formatOpts, locale) {\n    if (!formatOpts) {\n      return null;\n    }\n\n    const formatter = Formatter.create(locale, formatOpts);\n    const df = formatter.dtFormatter(getDummyDateTime());\n    const parts = df.formatToParts();\n    const resolvedOpts = df.resolvedOptions();\n    return parts.map((p) => tokenForPart(p, formatOpts, resolvedOpts));\n  }\n\n  const INVALID = \"Invalid DateTime\";\n  const MAX_DATE = 8.64e15;\n\n  function unsupportedZone(zone) {\n    return new Invalid(\"unsupported zone\", `the zone \"${zone.name}\" is not supported`);\n  }\n\n  // we cache week data on the DT object and this intermediates the cache\n  /**\n   * @param {DateTime} dt\n   */\n  function possiblyCachedWeekData(dt) {\n    if (dt.weekData === null) {\n      dt.weekData = gregorianToWeek(dt.c);\n    }\n    return dt.weekData;\n  }\n\n  /**\n   * @param {DateTime} dt\n   */\n  function possiblyCachedLocalWeekData(dt) {\n    if (dt.localWeekData === null) {\n      dt.localWeekData = gregorianToWeek(\n        dt.c,\n        dt.loc.getMinDaysInFirstWeek(),\n        dt.loc.getStartOfWeek()\n      );\n    }\n    return dt.localWeekData;\n  }\n\n  // clone really means, \"make a new object with these modifications\". all \"setters\" really use this\n  // to create a new object while only changing some of the properties\n  function clone(inst, alts) {\n    const current = {\n      ts: inst.ts,\n      zone: inst.zone,\n      c: inst.c,\n      o: inst.o,\n      loc: inst.loc,\n      invalid: inst.invalid,\n    };\n    return new DateTime({ ...current, ...alts, old: current });\n  }\n\n  // find the right offset a given local time. The o input is our guess, which determines which\n  // offset we'll pick in ambiguous cases (e.g. there are two 3 AMs b/c Fallback DST)\n  function fixOffset(localTS, o, tz) {\n    // Our UTC time is just a guess because our offset is just a guess\n    let utcGuess = localTS - o * 60 * 1000;\n\n    // Test whether the zone matches the offset for this ts\n    const o2 = tz.offset(utcGuess);\n\n    // If so, offset didn't change and we're done\n    if (o === o2) {\n      return [utcGuess, o];\n    }\n\n    // If not, change the ts by the difference in the offset\n    utcGuess -= (o2 - o) * 60 * 1000;\n\n    // If that gives us the local time we want, we're done\n    const o3 = tz.offset(utcGuess);\n    if (o2 === o3) {\n      return [utcGuess, o2];\n    }\n\n    // If it's different, we're in a hole time. The offset has changed, but the we don't adjust the time\n    return [localTS - Math.min(o2, o3) * 60 * 1000, Math.max(o2, o3)];\n  }\n\n  // convert an epoch timestamp into a calendar object with the given offset\n  function tsToObj(ts, offset) {\n    ts += offset * 60 * 1000;\n\n    const d = new Date(ts);\n\n    return {\n      year: d.getUTCFullYear(),\n      month: d.getUTCMonth() + 1,\n      day: d.getUTCDate(),\n      hour: d.getUTCHours(),\n      minute: d.getUTCMinutes(),\n      second: d.getUTCSeconds(),\n      millisecond: d.getUTCMilliseconds(),\n    };\n  }\n\n  // convert a calendar object to a epoch timestamp\n  function objToTS(obj, offset, zone) {\n    return fixOffset(objToLocalTS(obj), offset, zone);\n  }\n\n  // create a new DT instance by adding a duration, adjusting for DSTs\n  function adjustTime(inst, dur) {\n    const oPre = inst.o,\n      year = inst.c.year + Math.trunc(dur.years),\n      month = inst.c.month + Math.trunc(dur.months) + Math.trunc(dur.quarters) * 3,\n      c = {\n        ...inst.c,\n        year,\n        month,\n        day:\n          Math.min(inst.c.day, daysInMonth(year, month)) +\n          Math.trunc(dur.days) +\n          Math.trunc(dur.weeks) * 7,\n      },\n      millisToAdd = Duration.fromObject({\n        years: dur.years - Math.trunc(dur.years),\n        quarters: dur.quarters - Math.trunc(dur.quarters),\n        months: dur.months - Math.trunc(dur.months),\n        weeks: dur.weeks - Math.trunc(dur.weeks),\n        days: dur.days - Math.trunc(dur.days),\n        hours: dur.hours,\n        minutes: dur.minutes,\n        seconds: dur.seconds,\n        milliseconds: dur.milliseconds,\n      }).as(\"milliseconds\"),\n      localTS = objToLocalTS(c);\n\n    let [ts, o] = fixOffset(localTS, oPre, inst.zone);\n\n    if (millisToAdd !== 0) {\n      ts += millisToAdd;\n      // that could have changed the offset by going over a DST, but we want to keep the ts the same\n      o = inst.zone.offset(ts);\n    }\n\n    return { ts, o };\n  }\n\n  // helper useful in turning the results of parsing into real dates\n  // by handling the zone options\n  function parseDataToDateTime(parsed, parsedZone, opts, format, text, specificOffset) {\n    const { setZone, zone } = opts;\n    if ((parsed && Object.keys(parsed).length !== 0) || parsedZone) {\n      const interpretationZone = parsedZone || zone,\n        inst = DateTime.fromObject(parsed, {\n          ...opts,\n          zone: interpretationZone,\n          specificOffset,\n        });\n      return setZone ? inst : inst.setZone(zone);\n    } else {\n      return DateTime.invalid(\n        new Invalid(\"unparsable\", `the input \"${text}\" can't be parsed as ${format}`)\n      );\n    }\n  }\n\n  // if you want to output a technical format (e.g. RFC 2822), this helper\n  // helps handle the details\n  function toTechFormat(dt, format, allowZ = true) {\n    return dt.isValid\n      ? Formatter.create(Locale.create(\"en-US\"), {\n          allowZ,\n          forceSimple: true,\n        }).formatDateTimeFromString(dt, format)\n      : null;\n  }\n\n  function toISODate(o, extended) {\n    const longFormat = o.c.year > 9999 || o.c.year < 0;\n    let c = \"\";\n    if (longFormat && o.c.year >= 0) c += \"+\";\n    c += padStart(o.c.year, longFormat ? 6 : 4);\n\n    if (extended) {\n      c += \"-\";\n      c += padStart(o.c.month);\n      c += \"-\";\n      c += padStart(o.c.day);\n    } else {\n      c += padStart(o.c.month);\n      c += padStart(o.c.day);\n    }\n    return c;\n  }\n\n  function toISOTime(\n    o,\n    extended,\n    suppressSeconds,\n    suppressMilliseconds,\n    includeOffset,\n    extendedZone\n  ) {\n    let c = padStart(o.c.hour);\n    if (extended) {\n      c += \":\";\n      c += padStart(o.c.minute);\n      if (o.c.millisecond !== 0 || o.c.second !== 0 || !suppressSeconds) {\n        c += \":\";\n      }\n    } else {\n      c += padStart(o.c.minute);\n    }\n\n    if (o.c.millisecond !== 0 || o.c.second !== 0 || !suppressSeconds) {\n      c += padStart(o.c.second);\n\n      if (o.c.millisecond !== 0 || !suppressMilliseconds) {\n        c += \".\";\n        c += padStart(o.c.millisecond, 3);\n      }\n    }\n\n    if (includeOffset) {\n      if (o.isOffsetFixed && o.offset === 0 && !extendedZone) {\n        c += \"Z\";\n      } else if (o.o < 0) {\n        c += \"-\";\n        c += padStart(Math.trunc(-o.o / 60));\n        c += \":\";\n        c += padStart(Math.trunc(-o.o % 60));\n      } else {\n        c += \"+\";\n        c += padStart(Math.trunc(o.o / 60));\n        c += \":\";\n        c += padStart(Math.trunc(o.o % 60));\n      }\n    }\n\n    if (extendedZone) {\n      c += \"[\" + o.zone.ianaName + \"]\";\n    }\n    return c;\n  }\n\n  // defaults for unspecified units in the supported calendars\n  const defaultUnitValues = {\n      month: 1,\n      day: 1,\n      hour: 0,\n      minute: 0,\n      second: 0,\n      millisecond: 0,\n    },\n    defaultWeekUnitValues = {\n      weekNumber: 1,\n      weekday: 1,\n      hour: 0,\n      minute: 0,\n      second: 0,\n      millisecond: 0,\n    },\n    defaultOrdinalUnitValues = {\n      ordinal: 1,\n      hour: 0,\n      minute: 0,\n      second: 0,\n      millisecond: 0,\n    };\n\n  // Units in the supported calendars, sorted by bigness\n  const orderedUnits = [\"year\", \"month\", \"day\", \"hour\", \"minute\", \"second\", \"millisecond\"],\n    orderedWeekUnits = [\n      \"weekYear\",\n      \"weekNumber\",\n      \"weekday\",\n      \"hour\",\n      \"minute\",\n      \"second\",\n      \"millisecond\",\n    ],\n    orderedOrdinalUnits = [\"year\", \"ordinal\", \"hour\", \"minute\", \"second\", \"millisecond\"];\n\n  // standardize case and plurality in units\n  function normalizeUnit(unit) {\n    const normalized = {\n      year: \"year\",\n      years: \"year\",\n      month: \"month\",\n      months: \"month\",\n      day: \"day\",\n      days: \"day\",\n      hour: \"hour\",\n      hours: \"hour\",\n      minute: \"minute\",\n      minutes: \"minute\",\n      quarter: \"quarter\",\n      quarters: \"quarter\",\n      second: \"second\",\n      seconds: \"second\",\n      millisecond: \"millisecond\",\n      milliseconds: \"millisecond\",\n      weekday: \"weekday\",\n      weekdays: \"weekday\",\n      weeknumber: \"weekNumber\",\n      weeksnumber: \"weekNumber\",\n      weeknumbers: \"weekNumber\",\n      weekyear: \"weekYear\",\n      weekyears: \"weekYear\",\n      ordinal: \"ordinal\",\n    }[unit.toLowerCase()];\n\n    if (!normalized) throw new InvalidUnitError(unit);\n\n    return normalized;\n  }\n\n  function normalizeUnitWithLocalWeeks(unit) {\n    switch (unit.toLowerCase()) {\n      case \"localweekday\":\n      case \"localweekdays\":\n        return \"localWeekday\";\n      case \"localweeknumber\":\n      case \"localweeknumbers\":\n        return \"localWeekNumber\";\n      case \"localweekyear\":\n      case \"localweekyears\":\n        return \"localWeekYear\";\n      default:\n        return normalizeUnit(unit);\n    }\n  }\n\n  // this is a dumbed down version of fromObject() that runs about 60% faster\n  // but doesn't do any validation, makes a bunch of assumptions about what units\n  // are present, and so on.\n  function quickDT(obj, opts) {\n    const zone = normalizeZone(opts.zone, Settings.defaultZone),\n      loc = Locale.fromObject(opts),\n      tsNow = Settings.now();\n\n    let ts, o;\n\n    // assume we have the higher-order units\n    if (!isUndefined(obj.year)) {\n      for (const u of orderedUnits) {\n        if (isUndefined(obj[u])) {\n          obj[u] = defaultUnitValues[u];\n        }\n      }\n\n      const invalid = hasInvalidGregorianData(obj) || hasInvalidTimeData(obj);\n      if (invalid) {\n        return DateTime.invalid(invalid);\n      }\n\n      const offsetProvis = zone.offset(tsNow);\n      [ts, o] = objToTS(obj, offsetProvis, zone);\n    } else {\n      ts = tsNow;\n    }\n\n    return new DateTime({ ts, zone, loc, o });\n  }\n\n  function diffRelative(start, end, opts) {\n    const round = isUndefined(opts.round) ? true : opts.round,\n      format = (c, unit) => {\n        c = roundTo(c, round || opts.calendary ? 0 : 2, true);\n        const formatter = end.loc.clone(opts).relFormatter(opts);\n        return formatter.format(c, unit);\n      },\n      differ = (unit) => {\n        if (opts.calendary) {\n          if (!end.hasSame(start, unit)) {\n            return end.startOf(unit).diff(start.startOf(unit), unit).get(unit);\n          } else return 0;\n        } else {\n          return end.diff(start, unit).get(unit);\n        }\n      };\n\n    if (opts.unit) {\n      return format(differ(opts.unit), opts.unit);\n    }\n\n    for (const unit of opts.units) {\n      const count = differ(unit);\n      if (Math.abs(count) >= 1) {\n        return format(count, unit);\n      }\n    }\n    return format(start > end ? -0 : 0, opts.units[opts.units.length - 1]);\n  }\n\n  function lastOpts(argList) {\n    let opts = {},\n      args;\n    if (argList.length > 0 && typeof argList[argList.length - 1] === \"object\") {\n      opts = argList[argList.length - 1];\n      args = Array.from(argList).slice(0, argList.length - 1);\n    } else {\n      args = Array.from(argList);\n    }\n    return [opts, args];\n  }\n\n  /**\n   * A DateTime is an immutable data structure representing a specific date and time and accompanying methods. It contains class and instance methods for creating, parsing, interrogating, transforming, and formatting them.\n   *\n   * A DateTime comprises of:\n   * * A timestamp. Each DateTime instance refers to a specific millisecond of the Unix epoch.\n   * * A time zone. Each instance is considered in the context of a specific zone (by default the local system's zone).\n   * * Configuration properties that effect how output strings are formatted, such as `locale`, `numberingSystem`, and `outputCalendar`.\n   *\n   * Here is a brief overview of the most commonly used functionality it provides:\n   *\n   * * **Creation**: To create a DateTime from its components, use one of its factory class methods: {@link DateTime.local}, {@link DateTime.utc}, and (most flexibly) {@link DateTime.fromObject}. To create one from a standard string format, use {@link DateTime.fromISO}, {@link DateTime.fromHTTP}, and {@link DateTime.fromRFC2822}. To create one from a custom string format, use {@link DateTime.fromFormat}. To create one from a native JS date, use {@link DateTime.fromJSDate}.\n   * * **Gregorian calendar and time**: To examine the Gregorian properties of a DateTime individually (i.e as opposed to collectively through {@link DateTime#toObject}), use the {@link DateTime#year}, {@link DateTime#month},\n   * {@link DateTime#day}, {@link DateTime#hour}, {@link DateTime#minute}, {@link DateTime#second}, {@link DateTime#millisecond} accessors.\n   * * **Week calendar**: For ISO week calendar attributes, see the {@link DateTime#weekYear}, {@link DateTime#weekNumber}, and {@link DateTime#weekday} accessors.\n   * * **Configuration** See the {@link DateTime#locale} and {@link DateTime#numberingSystem} accessors.\n   * * **Transformation**: To transform the DateTime into other DateTimes, use {@link DateTime#set}, {@link DateTime#reconfigure}, {@link DateTime#setZone}, {@link DateTime#setLocale}, {@link DateTime.plus}, {@link DateTime#minus}, {@link DateTime#endOf}, {@link DateTime#startOf}, {@link DateTime#toUTC}, and {@link DateTime#toLocal}.\n   * * **Output**: To convert the DateTime to other representations, use the {@link DateTime#toRelative}, {@link DateTime#toRelativeCalendar}, {@link DateTime#toJSON}, {@link DateTime#toISO}, {@link DateTime#toHTTP}, {@link DateTime#toObject}, {@link DateTime#toRFC2822}, {@link DateTime#toString}, {@link DateTime#toLocaleString}, {@link DateTime#toFormat}, {@link DateTime#toMillis} and {@link DateTime#toJSDate}.\n   *\n   * There's plenty others documented below. In addition, for more information on subtler topics like internationalization, time zones, alternative calendars, validity, and so on, see the external documentation.\n   */\n  class DateTime {\n    /**\n     * @access private\n     */\n    constructor(config) {\n      const zone = config.zone || Settings.defaultZone;\n\n      let invalid =\n        config.invalid ||\n        (Number.isNaN(config.ts) ? new Invalid(\"invalid input\") : null) ||\n        (!zone.isValid ? unsupportedZone(zone) : null);\n      /**\n       * @access private\n       */\n      this.ts = isUndefined(config.ts) ? Settings.now() : config.ts;\n\n      let c = null,\n        o = null;\n      if (!invalid) {\n        const unchanged = config.old && config.old.ts === this.ts && config.old.zone.equals(zone);\n\n        if (unchanged) {\n          [c, o] = [config.old.c, config.old.o];\n        } else {\n          const ot = zone.offset(this.ts);\n          c = tsToObj(this.ts, ot);\n          invalid = Number.isNaN(c.year) ? new Invalid(\"invalid input\") : null;\n          c = invalid ? null : c;\n          o = invalid ? null : ot;\n        }\n      }\n\n      /**\n       * @access private\n       */\n      this._zone = zone;\n      /**\n       * @access private\n       */\n      this.loc = config.loc || Locale.create();\n      /**\n       * @access private\n       */\n      this.invalid = invalid;\n      /**\n       * @access private\n       */\n      this.weekData = null;\n      /**\n       * @access private\n       */\n      this.localWeekData = null;\n      /**\n       * @access private\n       */\n      this.c = c;\n      /**\n       * @access private\n       */\n      this.o = o;\n      /**\n       * @access private\n       */\n      this.isLuxonDateTime = true;\n    }\n\n    // CONSTRUCT\n\n    /**\n     * Create a DateTime for the current instant, in the system's time zone.\n     *\n     * Use Settings to override these default values if needed.\n     * @example DateTime.now().toISO() //~> now in the ISO format\n     * @return {DateTime}\n     */\n    static now() {\n      return new DateTime({});\n    }\n\n    /**\n     * Create a local DateTime\n     * @param {number} [year] - The calendar year. If omitted (as in, call `local()` with no arguments), the current time will be used\n     * @param {number} [month=1] - The month, 1-indexed\n     * @param {number} [day=1] - The day of the month, 1-indexed\n     * @param {number} [hour=0] - The hour of the day, in 24-hour time\n     * @param {number} [minute=0] - The minute of the hour, meaning a number between 0 and 59\n     * @param {number} [second=0] - The second of the minute, meaning a number between 0 and 59\n     * @param {number} [millisecond=0] - The millisecond of the second, meaning a number between 0 and 999\n     * @example DateTime.local()                                  //~> now\n     * @example DateTime.local({ zone: \"America/New_York\" })      //~> now, in US east coast time\n     * @example DateTime.local(2017)                              //~> 2017-01-01T00:00:00\n     * @example DateTime.local(2017, 3)                           //~> 2017-03-01T00:00:00\n     * @example DateTime.local(2017, 3, 12, { locale: \"fr\" })     //~> 2017-03-12T00:00:00, with a French locale\n     * @example DateTime.local(2017, 3, 12, 5)                    //~> 2017-03-12T05:00:00\n     * @example DateTime.local(2017, 3, 12, 5, { zone: \"utc\" })   //~> 2017-03-12T05:00:00, in UTC\n     * @example DateTime.local(2017, 3, 12, 5, 45)                //~> 2017-03-12T05:45:00\n     * @example DateTime.local(2017, 3, 12, 5, 45, 10)            //~> 2017-03-12T05:45:10\n     * @example DateTime.local(2017, 3, 12, 5, 45, 10, 765)       //~> 2017-03-12T05:45:10.765\n     * @return {DateTime}\n     */\n    static local() {\n      const [opts, args] = lastOpts(arguments),\n        [year, month, day, hour, minute, second, millisecond] = args;\n      return quickDT({ year, month, day, hour, minute, second, millisecond }, opts);\n    }\n\n    /**\n     * Create a DateTime in UTC\n     * @param {number} [year] - The calendar year. If omitted (as in, call `utc()` with no arguments), the current time will be used\n     * @param {number} [month=1] - The month, 1-indexed\n     * @param {number} [day=1] - The day of the month\n     * @param {number} [hour=0] - The hour of the day, in 24-hour time\n     * @param {number} [minute=0] - The minute of the hour, meaning a number between 0 and 59\n     * @param {number} [second=0] - The second of the minute, meaning a number between 0 and 59\n     * @param {number} [millisecond=0] - The millisecond of the second, meaning a number between 0 and 999\n     * @param {Object} options - configuration options for the DateTime\n     * @param {string} [options.locale] - a locale to set on the resulting DateTime instance\n     * @param {string} [options.outputCalendar] - the output calendar to set on the resulting DateTime instance\n     * @param {string} [options.numberingSystem] - the numbering system to set on the resulting DateTime instance\n     * @example DateTime.utc()                                              //~> now\n     * @example DateTime.utc(2017)                                          //~> 2017-01-01T00:00:00Z\n     * @example DateTime.utc(2017, 3)                                       //~> 2017-03-01T00:00:00Z\n     * @example DateTime.utc(2017, 3, 12)                                   //~> 2017-03-12T00:00:00Z\n     * @example DateTime.utc(2017, 3, 12, 5)                                //~> 2017-03-12T05:00:00Z\n     * @example DateTime.utc(2017, 3, 12, 5, 45)                            //~> 2017-03-12T05:45:00Z\n     * @example DateTime.utc(2017, 3, 12, 5, 45, { locale: \"fr\" })          //~> 2017-03-12T05:45:00Z with a French locale\n     * @example DateTime.utc(2017, 3, 12, 5, 45, 10)                        //~> 2017-03-12T05:45:10Z\n     * @example DateTime.utc(2017, 3, 12, 5, 45, 10, 765, { locale: \"fr\" }) //~> 2017-03-12T05:45:10.765Z with a French locale\n     * @return {DateTime}\n     */\n    static utc() {\n      const [opts, args] = lastOpts(arguments),\n        [year, month, day, hour, minute, second, millisecond] = args;\n\n      opts.zone = FixedOffsetZone.utcInstance;\n      return quickDT({ year, month, day, hour, minute, second, millisecond }, opts);\n    }\n\n    /**\n     * Create a DateTime from a JavaScript Date object. Uses the default zone.\n     * @param {Date} date - a JavaScript Date object\n     * @param {Object} options - configuration options for the DateTime\n     * @param {string|Zone} [options.zone='local'] - the zone to place the DateTime into\n     * @return {DateTime}\n     */\n    static fromJSDate(date, options = {}) {\n      const ts = isDate(date) ? date.valueOf() : NaN;\n      if (Number.isNaN(ts)) {\n        return DateTime.invalid(\"invalid input\");\n      }\n\n      const zoneToUse = normalizeZone(options.zone, Settings.defaultZone);\n      if (!zoneToUse.isValid) {\n        return DateTime.invalid(unsupportedZone(zoneToUse));\n      }\n\n      return new DateTime({\n        ts: ts,\n        zone: zoneToUse,\n        loc: Locale.fromObject(options),\n      });\n    }\n\n    /**\n     * Create a DateTime from a number of milliseconds since the epoch (meaning since 1 January 1970 00:00:00 UTC). Uses the default zone.\n     * @param {number} milliseconds - a number of milliseconds since 1970 UTC\n     * @param {Object} options - configuration options for the DateTime\n     * @param {string|Zone} [options.zone='local'] - the zone to place the DateTime into\n     * @param {string} [options.locale] - a locale to set on the resulting DateTime instance\n     * @param {string} options.outputCalendar - the output calendar to set on the resulting DateTime instance\n     * @param {string} options.numberingSystem - the numbering system to set on the resulting DateTime instance\n     * @return {DateTime}\n     */\n    static fromMillis(milliseconds, options = {}) {\n      if (!isNumber(milliseconds)) {\n        throw new InvalidArgumentError(\n          `fromMillis requires a numerical input, but received a ${typeof milliseconds} with value ${milliseconds}`\n        );\n      } else if (milliseconds < -MAX_DATE || milliseconds > MAX_DATE) {\n        // this isn't perfect because because we can still end up out of range because of additional shifting, but it's a start\n        return DateTime.invalid(\"Timestamp out of range\");\n      } else {\n        return new DateTime({\n          ts: milliseconds,\n          zone: normalizeZone(options.zone, Settings.defaultZone),\n          loc: Locale.fromObject(options),\n        });\n      }\n    }\n\n    /**\n     * Create a DateTime from a number of seconds since the epoch (meaning since 1 January 1970 00:00:00 UTC). Uses the default zone.\n     * @param {number} seconds - a number of seconds since 1970 UTC\n     * @param {Object} options - configuration options for the DateTime\n     * @param {string|Zone} [options.zone='local'] - the zone to place the DateTime into\n     * @param {string} [options.locale] - a locale to set on the resulting DateTime instance\n     * @param {string} options.outputCalendar - the output calendar to set on the resulting DateTime instance\n     * @param {string} options.numberingSystem - the numbering system to set on the resulting DateTime instance\n     * @return {DateTime}\n     */\n    static fromSeconds(seconds, options = {}) {\n      if (!isNumber(seconds)) {\n        throw new InvalidArgumentError(\"fromSeconds requires a numerical input\");\n      } else {\n        return new DateTime({\n          ts: seconds * 1000,\n          zone: normalizeZone(options.zone, Settings.defaultZone),\n          loc: Locale.fromObject(options),\n        });\n      }\n    }\n\n    /**\n     * Create a DateTime from a JavaScript object with keys like 'year' and 'hour' with reasonable defaults.\n     * @param {Object} obj - the object to create the DateTime from\n     * @param {number} obj.year - a year, such as 1987\n     * @param {number} obj.month - a month, 1-12\n     * @param {number} obj.day - a day of the month, 1-31, depending on the month\n     * @param {number} obj.ordinal - day of the year, 1-365 or 366\n     * @param {number} obj.weekYear - an ISO week year\n     * @param {number} obj.weekNumber - an ISO week number, between 1 and 52 or 53, depending on the year\n     * @param {number} obj.weekday - an ISO weekday, 1-7, where 1 is Monday and 7 is Sunday\n     * @param {number} obj.localWeekYear - a week year, according to the locale\n     * @param {number} obj.localWeekNumber - a week number, between 1 and 52 or 53, depending on the year, according to the locale\n     * @param {number} obj.localWeekday - a weekday, 1-7, where 1 is the first and 7 is the last day of the week, according to the locale\n     * @param {number} obj.hour - hour of the day, 0-23\n     * @param {number} obj.minute - minute of the hour, 0-59\n     * @param {number} obj.second - second of the minute, 0-59\n     * @param {number} obj.millisecond - millisecond of the second, 0-999\n     * @param {Object} opts - options for creating this DateTime\n     * @param {string|Zone} [opts.zone='local'] - interpret the numbers in the context of a particular zone. Can take any value taken as the first argument to setZone()\n     * @param {string} [opts.locale='system\\'s locale'] - a locale to set on the resulting DateTime instance\n     * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance\n     * @param {string} opts.numberingSystem - the numbering system to set on the resulting DateTime instance\n     * @example DateTime.fromObject({ year: 1982, month: 5, day: 25}).toISODate() //=> '1982-05-25'\n     * @example DateTime.fromObject({ year: 1982 }).toISODate() //=> '1982-01-01'\n     * @example DateTime.fromObject({ hour: 10, minute: 26, second: 6 }) //~> today at 10:26:06\n     * @example DateTime.fromObject({ hour: 10, minute: 26, second: 6 }, { zone: 'utc' }),\n     * @example DateTime.fromObject({ hour: 10, minute: 26, second: 6 }, { zone: 'local' })\n     * @example DateTime.fromObject({ hour: 10, minute: 26, second: 6 }, { zone: 'America/New_York' })\n     * @example DateTime.fromObject({ weekYear: 2016, weekNumber: 2, weekday: 3 }).toISODate() //=> '2016-01-13'\n     * @example DateTime.fromObject({ localWeekYear: 2022, localWeekNumber: 1, localWeekday: 1 }, { locale: \"en-US\" }).toISODate() //=> '2021-12-26'\n     * @return {DateTime}\n     */\n    static fromObject(obj, opts = {}) {\n      obj = obj || {};\n      const zoneToUse = normalizeZone(opts.zone, Settings.defaultZone);\n      if (!zoneToUse.isValid) {\n        return DateTime.invalid(unsupportedZone(zoneToUse));\n      }\n\n      const loc = Locale.fromObject(opts);\n      const normalized = normalizeObject(obj, normalizeUnitWithLocalWeeks);\n      const { minDaysInFirstWeek, startOfWeek } = usesLocalWeekValues(normalized, loc);\n\n      const tsNow = Settings.now(),\n        offsetProvis = !isUndefined(opts.specificOffset)\n          ? opts.specificOffset\n          : zoneToUse.offset(tsNow),\n        containsOrdinal = !isUndefined(normalized.ordinal),\n        containsGregorYear = !isUndefined(normalized.year),\n        containsGregorMD = !isUndefined(normalized.month) || !isUndefined(normalized.day),\n        containsGregor = containsGregorYear || containsGregorMD,\n        definiteWeekDef = normalized.weekYear || normalized.weekNumber;\n\n      // cases:\n      // just a weekday -> this week's instance of that weekday, no worries\n      // (gregorian data or ordinal) + (weekYear or weekNumber) -> error\n      // (gregorian month or day) + ordinal -> error\n      // otherwise just use weeks or ordinals or gregorian, depending on what's specified\n\n      if ((containsGregor || containsOrdinal) && definiteWeekDef) {\n        throw new ConflictingSpecificationError(\n          \"Can't mix weekYear/weekNumber units with year/month/day or ordinals\"\n        );\n      }\n\n      if (containsGregorMD && containsOrdinal) {\n        throw new ConflictingSpecificationError(\"Can't mix ordinal dates with month/day\");\n      }\n\n      const useWeekData = definiteWeekDef || (normalized.weekday && !containsGregor);\n\n      // configure ourselves to deal with gregorian dates or week stuff\n      let units,\n        defaultValues,\n        objNow = tsToObj(tsNow, offsetProvis);\n      if (useWeekData) {\n        units = orderedWeekUnits;\n        defaultValues = defaultWeekUnitValues;\n        objNow = gregorianToWeek(objNow, minDaysInFirstWeek, startOfWeek);\n      } else if (containsOrdinal) {\n        units = orderedOrdinalUnits;\n        defaultValues = defaultOrdinalUnitValues;\n        objNow = gregorianToOrdinal(objNow);\n      } else {\n        units = orderedUnits;\n        defaultValues = defaultUnitValues;\n      }\n\n      // set default values for missing stuff\n      let foundFirst = false;\n      for (const u of units) {\n        const v = normalized[u];\n        if (!isUndefined(v)) {\n          foundFirst = true;\n        } else if (foundFirst) {\n          normalized[u] = defaultValues[u];\n        } else {\n          normalized[u] = objNow[u];\n        }\n      }\n\n      // make sure the values we have are in range\n      const higherOrderInvalid = useWeekData\n          ? hasInvalidWeekData(normalized, minDaysInFirstWeek, startOfWeek)\n          : containsOrdinal\n          ? hasInvalidOrdinalData(normalized)\n          : hasInvalidGregorianData(normalized),\n        invalid = higherOrderInvalid || hasInvalidTimeData(normalized);\n\n      if (invalid) {\n        return DateTime.invalid(invalid);\n      }\n\n      // compute the actual time\n      const gregorian = useWeekData\n          ? weekToGregorian(normalized, minDaysInFirstWeek, startOfWeek)\n          : containsOrdinal\n          ? ordinalToGregorian(normalized)\n          : normalized,\n        [tsFinal, offsetFinal] = objToTS(gregorian, offsetProvis, zoneToUse),\n        inst = new DateTime({\n          ts: tsFinal,\n          zone: zoneToUse,\n          o: offsetFinal,\n          loc,\n        });\n\n      // gregorian data + weekday serves only to validate\n      if (normalized.weekday && containsGregor && obj.weekday !== inst.weekday) {\n        return DateTime.invalid(\n          \"mismatched weekday\",\n          `you can't specify both a weekday of ${normalized.weekday} and a date of ${inst.toISO()}`\n        );\n      }\n\n      if (!inst.isValid) {\n        return DateTime.invalid(inst.invalid);\n      }\n\n      return inst;\n    }\n\n    /**\n     * Create a DateTime from an ISO 8601 string\n     * @param {string} text - the ISO string\n     * @param {Object} opts - options to affect the creation\n     * @param {string|Zone} [opts.zone='local'] - use this zone if no offset is specified in the input string itself. Will also convert the time to this zone\n     * @param {boolean} [opts.setZone=false] - override the zone with a fixed-offset zone specified in the string itself, if it specifies one\n     * @param {string} [opts.locale='system's locale'] - a locale to set on the resulting DateTime instance\n     * @param {string} [opts.outputCalendar] - the output calendar to set on the resulting DateTime instance\n     * @param {string} [opts.numberingSystem] - the numbering system to set on the resulting DateTime instance\n     * @example DateTime.fromISO('2016-05-25T09:08:34.123')\n     * @example DateTime.fromISO('2016-05-25T09:08:34.123+06:00')\n     * @example DateTime.fromISO('2016-05-25T09:08:34.123+06:00', {setZone: true})\n     * @example DateTime.fromISO('2016-05-25T09:08:34.123', {zone: 'utc'})\n     * @example DateTime.fromISO('2016-W05-4')\n     * @return {DateTime}\n     */\n    static fromISO(text, opts = {}) {\n      const [vals, parsedZone] = parseISODate(text);\n      return parseDataToDateTime(vals, parsedZone, opts, \"ISO 8601\", text);\n    }\n\n    /**\n     * Create a DateTime from an RFC 2822 string\n     * @param {string} text - the RFC 2822 string\n     * @param {Object} opts - options to affect the creation\n     * @param {string|Zone} [opts.zone='local'] - convert the time to this zone. Since the offset is always specified in the string itself, this has no effect on the interpretation of string, merely the zone the resulting DateTime is expressed in.\n     * @param {boolean} [opts.setZone=false] - override the zone with a fixed-offset zone specified in the string itself, if it specifies one\n     * @param {string} [opts.locale='system's locale'] - a locale to set on the resulting DateTime instance\n     * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance\n     * @param {string} opts.numberingSystem - the numbering system to set on the resulting DateTime instance\n     * @example DateTime.fromRFC2822('25 Nov 2016 13:23:12 GMT')\n     * @example DateTime.fromRFC2822('Fri, 25 Nov 2016 13:23:12 +0600')\n     * @example DateTime.fromRFC2822('25 Nov 2016 13:23 Z')\n     * @return {DateTime}\n     */\n    static fromRFC2822(text, opts = {}) {\n      const [vals, parsedZone] = parseRFC2822Date(text);\n      return parseDataToDateTime(vals, parsedZone, opts, \"RFC 2822\", text);\n    }\n\n    /**\n     * Create a DateTime from an HTTP header date\n     * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1\n     * @param {string} text - the HTTP header date\n     * @param {Object} opts - options to affect the creation\n     * @param {string|Zone} [opts.zone='local'] - convert the time to this zone. Since HTTP dates are always in UTC, this has no effect on the interpretation of string, merely the zone the resulting DateTime is expressed in.\n     * @param {boolean} [opts.setZone=false] - override the zone with the fixed-offset zone specified in the string. For HTTP dates, this is always UTC, so this option is equivalent to setting the `zone` option to 'utc', but this option is included for consistency with similar methods.\n     * @param {string} [opts.locale='system's locale'] - a locale to set on the resulting DateTime instance\n     * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance\n     * @param {string} opts.numberingSystem - the numbering system to set on the resulting DateTime instance\n     * @example DateTime.fromHTTP('Sun, 06 Nov 1994 08:49:37 GMT')\n     * @example DateTime.fromHTTP('Sunday, 06-Nov-94 08:49:37 GMT')\n     * @example DateTime.fromHTTP('Sun Nov  6 08:49:37 1994')\n     * @return {DateTime}\n     */\n    static fromHTTP(text, opts = {}) {\n      const [vals, parsedZone] = parseHTTPDate(text);\n      return parseDataToDateTime(vals, parsedZone, opts, \"HTTP\", opts);\n    }\n\n    /**\n     * Create a DateTime from an input string and format string.\n     * Defaults to en-US if no locale has been specified, regardless of the system's locale. For a table of tokens and their interpretations, see [here](https://moment.github.io/luxon/#/parsing?id=table-of-tokens).\n     * @param {string} text - the string to parse\n     * @param {string} fmt - the format the string is expected to be in (see the link below for the formats)\n     * @param {Object} opts - options to affect the creation\n     * @param {string|Zone} [opts.zone='local'] - use this zone if no offset is specified in the input string itself. Will also convert the DateTime to this zone\n     * @param {boolean} [opts.setZone=false] - override the zone with a zone specified in the string itself, if it specifies one\n     * @param {string} [opts.locale='en-US'] - a locale string to use when parsing. Will also set the DateTime to this locale\n     * @param {string} opts.numberingSystem - the numbering system to use when parsing. Will also set the resulting DateTime to this numbering system\n     * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance\n     * @return {DateTime}\n     */\n    static fromFormat(text, fmt, opts = {}) {\n      if (isUndefined(text) || isUndefined(fmt)) {\n        throw new InvalidArgumentError(\"fromFormat requires an input string and a format\");\n      }\n\n      const { locale = null, numberingSystem = null } = opts,\n        localeToUse = Locale.fromOpts({\n          locale,\n          numberingSystem,\n          defaultToEN: true,\n        }),\n        [vals, parsedZone, specificOffset, invalid] = parseFromTokens(localeToUse, text, fmt);\n      if (invalid) {\n        return DateTime.invalid(invalid);\n      } else {\n        return parseDataToDateTime(vals, parsedZone, opts, `format ${fmt}`, text, specificOffset);\n      }\n    }\n\n    /**\n     * @deprecated use fromFormat instead\n     */\n    static fromString(text, fmt, opts = {}) {\n      return DateTime.fromFormat(text, fmt, opts);\n    }\n\n    /**\n     * Create a DateTime from a SQL date, time, or datetime\n     * Defaults to en-US if no locale has been specified, regardless of the system's locale\n     * @param {string} text - the string to parse\n     * @param {Object} opts - options to affect the creation\n     * @param {string|Zone} [opts.zone='local'] - use this zone if no offset is specified in the input string itself. Will also convert the DateTime to this zone\n     * @param {boolean} [opts.setZone=false] - override the zone with a zone specified in the string itself, if it specifies one\n     * @param {string} [opts.locale='en-US'] - a locale string to use when parsing. Will also set the DateTime to this locale\n     * @param {string} opts.numberingSystem - the numbering system to use when parsing. Will also set the resulting DateTime to this numbering system\n     * @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance\n     * @example DateTime.fromSQL('2017-05-15')\n     * @example DateTime.fromSQL('2017-05-15 09:12:34')\n     * @example DateTime.fromSQL('2017-05-15 09:12:34.342')\n     * @example DateTime.fromSQL('2017-05-15 09:12:34.342+06:00')\n     * @example DateTime.fromSQL('2017-05-15 09:12:34.342 America/Los_Angeles')\n     * @example DateTime.fromSQL('2017-05-15 09:12:34.342 America/Los_Angeles', { setZone: true })\n     * @example DateTime.fromSQL('2017-05-15 09:12:34.342', { zone: 'America/Los_Angeles' })\n     * @example DateTime.fromSQL('09:12:34.342')\n     * @return {DateTime}\n     */\n    static fromSQL(text, opts = {}) {\n      const [vals, parsedZone] = parseSQL(text);\n      return parseDataToDateTime(vals, parsedZone, opts, \"SQL\", text);\n    }\n\n    /**\n     * Create an invalid DateTime.\n     * @param {string} reason - simple string of why this DateTime is invalid. Should not contain parameters or anything else data-dependent.\n     * @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information\n     * @return {DateTime}\n     */\n    static invalid(reason, explanation = null) {\n      if (!reason) {\n        throw new InvalidArgumentError(\"need to specify a reason the DateTime is invalid\");\n      }\n\n      const invalid = reason instanceof Invalid ? reason : new Invalid(reason, explanation);\n\n      if (Settings.throwOnInvalid) {\n        throw new InvalidDateTimeError(invalid);\n      } else {\n        return new DateTime({ invalid });\n      }\n    }\n\n    /**\n     * Check if an object is an instance of DateTime. Works across context boundaries\n     * @param {object} o\n     * @return {boolean}\n     */\n    static isDateTime(o) {\n      return (o && o.isLuxonDateTime) || false;\n    }\n\n    /**\n     * Produce the format string for a set of options\n     * @param formatOpts\n     * @param localeOpts\n     * @returns {string}\n     */\n    static parseFormatForOpts(formatOpts, localeOpts = {}) {\n      const tokenList = formatOptsToTokens(formatOpts, Locale.fromObject(localeOpts));\n      return !tokenList ? null : tokenList.map((t) => (t ? t.val : null)).join(\"\");\n    }\n\n    /**\n     * Produce the the fully expanded format token for the locale\n     * Does NOT quote characters, so quoted tokens will not round trip correctly\n     * @param fmt\n     * @param localeOpts\n     * @returns {string}\n     */\n    static expandFormat(fmt, localeOpts = {}) {\n      const expanded = expandMacroTokens(Formatter.parseFormat(fmt), Locale.fromObject(localeOpts));\n      return expanded.map((t) => t.val).join(\"\");\n    }\n\n    // INFO\n\n    /**\n     * Get the value of unit.\n     * @param {string} unit - a unit such as 'minute' or 'day'\n     * @example DateTime.local(2017, 7, 4).get('month'); //=> 7\n     * @example DateTime.local(2017, 7, 4).get('day'); //=> 4\n     * @return {number}\n     */\n    get(unit) {\n      return this[unit];\n    }\n\n    /**\n     * Returns whether the DateTime is valid. Invalid DateTimes occur when:\n     * * The DateTime was created from invalid calendar information, such as the 13th month or February 30\n     * * The DateTime was created by an operation on another invalid date\n     * @type {boolean}\n     */\n    get isValid() {\n      return this.invalid === null;\n    }\n\n    /**\n     * Returns an error code if this DateTime is invalid, or null if the DateTime is valid\n     * @type {string}\n     */\n    get invalidReason() {\n      return this.invalid ? this.invalid.reason : null;\n    }\n\n    /**\n     * Returns an explanation of why this DateTime became invalid, or null if the DateTime is valid\n     * @type {string}\n     */\n    get invalidExplanation() {\n      return this.invalid ? this.invalid.explanation : null;\n    }\n\n    /**\n     * Get the locale of a DateTime, such 'en-GB'. The locale is used when formatting the DateTime\n     *\n     * @type {string}\n     */\n    get locale() {\n      return this.isValid ? this.loc.locale : null;\n    }\n\n    /**\n     * Get the numbering system of a DateTime, such 'beng'. The numbering system is used when formatting the DateTime\n     *\n     * @type {string}\n     */\n    get numberingSystem() {\n      return this.isValid ? this.loc.numberingSystem : null;\n    }\n\n    /**\n     * Get the output calendar of a DateTime, such 'islamic'. The output calendar is used when formatting the DateTime\n     *\n     * @type {string}\n     */\n    get outputCalendar() {\n      return this.isValid ? this.loc.outputCalendar : null;\n    }\n\n    /**\n     * Get the time zone associated with this DateTime.\n     * @type {Zone}\n     */\n    get zone() {\n      return this._zone;\n    }\n\n    /**\n     * Get the name of the time zone.\n     * @type {string}\n     */\n    get zoneName() {\n      return this.isValid ? this.zone.name : null;\n    }\n\n    /**\n     * Get the year\n     * @example DateTime.local(2017, 5, 25).year //=> 2017\n     * @type {number}\n     */\n    get year() {\n      return this.isValid ? this.c.year : NaN;\n    }\n\n    /**\n     * Get the quarter\n     * @example DateTime.local(2017, 5, 25).quarter //=> 2\n     * @type {number}\n     */\n    get quarter() {\n      return this.isValid ? Math.ceil(this.c.month / 3) : NaN;\n    }\n\n    /**\n     * Get the month (1-12).\n     * @example DateTime.local(2017, 5, 25).month //=> 5\n     * @type {number}\n     */\n    get month() {\n      return this.isValid ? this.c.month : NaN;\n    }\n\n    /**\n     * Get the day of the month (1-30ish).\n     * @example DateTime.local(2017, 5, 25).day //=> 25\n     * @type {number}\n     */\n    get day() {\n      return this.isValid ? this.c.day : NaN;\n    }\n\n    /**\n     * Get the hour of the day (0-23).\n     * @example DateTime.local(2017, 5, 25, 9).hour //=> 9\n     * @type {number}\n     */\n    get hour() {\n      return this.isValid ? this.c.hour : NaN;\n    }\n\n    /**\n     * Get the minute of the hour (0-59).\n     * @example DateTime.local(2017, 5, 25, 9, 30).minute //=> 30\n     * @type {number}\n     */\n    get minute() {\n      return this.isValid ? this.c.minute : NaN;\n    }\n\n    /**\n     * Get the second of the minute (0-59).\n     * @example DateTime.local(2017, 5, 25, 9, 30, 52).second //=> 52\n     * @type {number}\n     */\n    get second() {\n      return this.isValid ? this.c.second : NaN;\n    }\n\n    /**\n     * Get the millisecond of the second (0-999).\n     * @example DateTime.local(2017, 5, 25, 9, 30, 52, 654).millisecond //=> 654\n     * @type {number}\n     */\n    get millisecond() {\n      return this.isValid ? this.c.millisecond : NaN;\n    }\n\n    /**\n     * Get the week year\n     * @see https://en.wikipedia.org/wiki/ISO_week_date\n     * @example DateTime.local(2014, 12, 31).weekYear //=> 2015\n     * @type {number}\n     */\n    get weekYear() {\n      return this.isValid ? possiblyCachedWeekData(this).weekYear : NaN;\n    }\n\n    /**\n     * Get the week number of the week year (1-52ish).\n     * @see https://en.wikipedia.org/wiki/ISO_week_date\n     * @example DateTime.local(2017, 5, 25).weekNumber //=> 21\n     * @type {number}\n     */\n    get weekNumber() {\n      return this.isValid ? possiblyCachedWeekData(this).weekNumber : NaN;\n    }\n\n    /**\n     * Get the day of the week.\n     * 1 is Monday and 7 is Sunday\n     * @see https://en.wikipedia.org/wiki/ISO_week_date\n     * @example DateTime.local(2014, 11, 31).weekday //=> 4\n     * @type {number}\n     */\n    get weekday() {\n      return this.isValid ? possiblyCachedWeekData(this).weekday : NaN;\n    }\n\n    /**\n     * Returns true if this date is on a weekend according to the locale, false otherwise\n     * @returns {boolean}\n     */\n    get isWeekend() {\n      return this.isValid && this.loc.getWeekendDays().includes(this.weekday);\n    }\n\n    /**\n     * Get the day of the week according to the locale.\n     * 1 is the first day of the week and 7 is the last day of the week.\n     * If the locale assigns Sunday as the first day of the week, then a date which is a Sunday will return 1,\n     * @returns {number}\n     */\n    get localWeekday() {\n      return this.isValid ? possiblyCachedLocalWeekData(this).weekday : NaN;\n    }\n\n    /**\n     * Get the week number of the week year according to the locale. Different locales assign week numbers differently,\n     * because the week can start on different days of the week (see localWeekday) and because a different number of days\n     * is required for a week to count as the first week of a year.\n     * @returns {number}\n     */\n    get localWeekNumber() {\n      return this.isValid ? possiblyCachedLocalWeekData(this).weekNumber : NaN;\n    }\n\n    /**\n     * Get the week year according to the locale. Different locales assign week numbers (and therefor week years)\n     * differently, see localWeekNumber.\n     * @returns {number}\n     */\n    get localWeekYear() {\n      return this.isValid ? possiblyCachedLocalWeekData(this).weekYear : NaN;\n    }\n\n    /**\n     * Get the ordinal (meaning the day of the year)\n     * @example DateTime.local(2017, 5, 25).ordinal //=> 145\n     * @type {number|DateTime}\n     */\n    get ordinal() {\n      return this.isValid ? gregorianToOrdinal(this.c).ordinal : NaN;\n    }\n\n    /**\n     * Get the human readable short month name, such as 'Oct'.\n     * Defaults to the system's locale if no locale has been specified\n     * @example DateTime.local(2017, 10, 30).monthShort //=> Oct\n     * @type {string}\n     */\n    get monthShort() {\n      return this.isValid ? Info.months(\"short\", { locObj: this.loc })[this.month - 1] : null;\n    }\n\n    /**\n     * Get the human readable long month name, such as 'October'.\n     * Defaults to the system's locale if no locale has been specified\n     * @example DateTime.local(2017, 10, 30).monthLong //=> October\n     * @type {string}\n     */\n    get monthLong() {\n      return this.isValid ? Info.months(\"long\", { locObj: this.loc })[this.month - 1] : null;\n    }\n\n    /**\n     * Get the human readable short weekday, such as 'Mon'.\n     * Defaults to the system's locale if no locale has been specified\n     * @example DateTime.local(2017, 10, 30).weekdayShort //=> Mon\n     * @type {string}\n     */\n    get weekdayShort() {\n      return this.isValid ? Info.weekdays(\"short\", { locObj: this.loc })[this.weekday - 1] : null;\n    }\n\n    /**\n     * Get the human readable long weekday, such as 'Monday'.\n     * Defaults to the system's locale if no locale has been specified\n     * @example DateTime.local(2017, 10, 30).weekdayLong //=> Monday\n     * @type {string}\n     */\n    get weekdayLong() {\n      return this.isValid ? Info.weekdays(\"long\", { locObj: this.loc })[this.weekday - 1] : null;\n    }\n\n    /**\n     * Get the UTC offset of this DateTime in minutes\n     * @example DateTime.now().offset //=> -240\n     * @example DateTime.utc().offset //=> 0\n     * @type {number}\n     */\n    get offset() {\n      return this.isValid ? +this.o : NaN;\n    }\n\n    /**\n     * Get the short human name for the zone's current offset, for example \"EST\" or \"EDT\".\n     * Defaults to the system's locale if no locale has been specified\n     * @type {string}\n     */\n    get offsetNameShort() {\n      if (this.isValid) {\n        return this.zone.offsetName(this.ts, {\n          format: \"short\",\n          locale: this.locale,\n        });\n      } else {\n        return null;\n      }\n    }\n\n    /**\n     * Get the long human name for the zone's current offset, for example \"Eastern Standard Time\" or \"Eastern Daylight Time\".\n     * Defaults to the system's locale if no locale has been specified\n     * @type {string}\n     */\n    get offsetNameLong() {\n      if (this.isValid) {\n        return this.zone.offsetName(this.ts, {\n          format: \"long\",\n          locale: this.locale,\n        });\n      } else {\n        return null;\n      }\n    }\n\n    /**\n     * Get whether this zone's offset ever changes, as in a DST.\n     * @type {boolean}\n     */\n    get isOffsetFixed() {\n      return this.isValid ? this.zone.isUniversal : null;\n    }\n\n    /**\n     * Get whether the DateTime is in a DST.\n     * @type {boolean}\n     */\n    get isInDST() {\n      if (this.isOffsetFixed) {\n        return false;\n      } else {\n        return (\n          this.offset > this.set({ month: 1, day: 1 }).offset ||\n          this.offset > this.set({ month: 5 }).offset\n        );\n      }\n    }\n\n    /**\n     * Get those DateTimes which have the same local time as this DateTime, but a different offset from UTC\n     * in this DateTime's zone. During DST changes local time can be ambiguous, for example\n     * `2023-10-29T02:30:00` in `Europe/Berlin` can have offset `+01:00` or `+02:00`.\n     * This method will return both possible DateTimes if this DateTime's local time is ambiguous.\n     * @returns {DateTime[]}\n     */\n    getPossibleOffsets() {\n      if (!this.isValid || this.isOffsetFixed) {\n        return [this];\n      }\n      const dayMs = 86400000;\n      const minuteMs = 60000;\n      const localTS = objToLocalTS(this.c);\n      const oEarlier = this.zone.offset(localTS - dayMs);\n      const oLater = this.zone.offset(localTS + dayMs);\n\n      const o1 = this.zone.offset(localTS - oEarlier * minuteMs);\n      const o2 = this.zone.offset(localTS - oLater * minuteMs);\n      if (o1 === o2) {\n        return [this];\n      }\n      const ts1 = localTS - o1 * minuteMs;\n      const ts2 = localTS - o2 * minuteMs;\n      const c1 = tsToObj(ts1, o1);\n      const c2 = tsToObj(ts2, o2);\n      if (\n        c1.hour === c2.hour &&\n        c1.minute === c2.minute &&\n        c1.second === c2.second &&\n        c1.millisecond === c2.millisecond\n      ) {\n        return [clone(this, { ts: ts1 }), clone(this, { ts: ts2 })];\n      }\n      return [this];\n    }\n\n    /**\n     * Returns true if this DateTime is in a leap year, false otherwise\n     * @example DateTime.local(2016).isInLeapYear //=> true\n     * @example DateTime.local(2013).isInLeapYear //=> false\n     * @type {boolean}\n     */\n    get isInLeapYear() {\n      return isLeapYear(this.year);\n    }\n\n    /**\n     * Returns the number of days in this DateTime's month\n     * @example DateTime.local(2016, 2).daysInMonth //=> 29\n     * @example DateTime.local(2016, 3).daysInMonth //=> 31\n     * @type {number}\n     */\n    get daysInMonth() {\n      return daysInMonth(this.year, this.month);\n    }\n\n    /**\n     * Returns the number of days in this DateTime's year\n     * @example DateTime.local(2016).daysInYear //=> 366\n     * @example DateTime.local(2013).daysInYear //=> 365\n     * @type {number}\n     */\n    get daysInYear() {\n      return this.isValid ? daysInYear(this.year) : NaN;\n    }\n\n    /**\n     * Returns the number of weeks in this DateTime's year\n     * @see https://en.wikipedia.org/wiki/ISO_week_date\n     * @example DateTime.local(2004).weeksInWeekYear //=> 53\n     * @example DateTime.local(2013).weeksInWeekYear //=> 52\n     * @type {number}\n     */\n    get weeksInWeekYear() {\n      return this.isValid ? weeksInWeekYear(this.weekYear) : NaN;\n    }\n\n    /**\n     * Returns the number of weeks in this DateTime's local week year\n     * @example DateTime.local(2020, 6, {locale: 'en-US'}).weeksInLocalWeekYear //=> 52\n     * @example DateTime.local(2020, 6, {locale: 'de-DE'}).weeksInLocalWeekYear //=> 53\n     * @type {number}\n     */\n    get weeksInLocalWeekYear() {\n      return this.isValid\n        ? weeksInWeekYear(\n            this.localWeekYear,\n            this.loc.getMinDaysInFirstWeek(),\n            this.loc.getStartOfWeek()\n          )\n        : NaN;\n    }\n\n    /**\n     * Returns the resolved Intl options for this DateTime.\n     * This is useful in understanding the behavior of formatting methods\n     * @param {Object} opts - the same options as toLocaleString\n     * @return {Object}\n     */\n    resolvedLocaleOptions(opts = {}) {\n      const { locale, numberingSystem, calendar } = Formatter.create(\n        this.loc.clone(opts),\n        opts\n      ).resolvedOptions(this);\n      return { locale, numberingSystem, outputCalendar: calendar };\n    }\n\n    // TRANSFORM\n\n    /**\n     * \"Set\" the DateTime's zone to UTC. Returns a newly-constructed DateTime.\n     *\n     * Equivalent to {@link DateTime#setZone}('utc')\n     * @param {number} [offset=0] - optionally, an offset from UTC in minutes\n     * @param {Object} [opts={}] - options to pass to `setZone()`\n     * @return {DateTime}\n     */\n    toUTC(offset = 0, opts = {}) {\n      return this.setZone(FixedOffsetZone.instance(offset), opts);\n    }\n\n    /**\n     * \"Set\" the DateTime's zone to the host's local zone. Returns a newly-constructed DateTime.\n     *\n     * Equivalent to `setZone('local')`\n     * @return {DateTime}\n     */\n    toLocal() {\n      return this.setZone(Settings.defaultZone);\n    }\n\n    /**\n     * \"Set\" the DateTime's zone to specified zone. Returns a newly-constructed DateTime.\n     *\n     * By default, the setter keeps the underlying time the same (as in, the same timestamp), but the new instance will report different local times and consider DSTs when making computations, as with {@link DateTime#plus}. You may wish to use {@link DateTime#toLocal} and {@link DateTime#toUTC} which provide simple convenience wrappers for commonly used zones.\n     * @param {string|Zone} [zone='local'] - a zone identifier. As a string, that can be any IANA zone supported by the host environment, or a fixed-offset name of the form 'UTC+3', or the strings 'local' or 'utc'. You may also supply an instance of a {@link DateTime#Zone} class.\n     * @param {Object} opts - options\n     * @param {boolean} [opts.keepLocalTime=false] - If true, adjust the underlying time so that the local time stays the same, but in the target zone. You should rarely need this.\n     * @return {DateTime}\n     */\n    setZone(zone, { keepLocalTime = false, keepCalendarTime = false } = {}) {\n      zone = normalizeZone(zone, Settings.defaultZone);\n      if (zone.equals(this.zone)) {\n        return this;\n      } else if (!zone.isValid) {\n        return DateTime.invalid(unsupportedZone(zone));\n      } else {\n        let newTS = this.ts;\n        if (keepLocalTime || keepCalendarTime) {\n          const offsetGuess = zone.offset(this.ts);\n          const asObj = this.toObject();\n          [newTS] = objToTS(asObj, offsetGuess, zone);\n        }\n        return clone(this, { ts: newTS, zone });\n      }\n    }\n\n    /**\n     * \"Set\" the locale, numberingSystem, or outputCalendar. Returns a newly-constructed DateTime.\n     * @param {Object} properties - the properties to set\n     * @example DateTime.local(2017, 5, 25).reconfigure({ locale: 'en-GB' })\n     * @return {DateTime}\n     */\n    reconfigure({ locale, numberingSystem, outputCalendar } = {}) {\n      const loc = this.loc.clone({ locale, numberingSystem, outputCalendar });\n      return clone(this, { loc });\n    }\n\n    /**\n     * \"Set\" the locale. Returns a newly-constructed DateTime.\n     * Just a convenient alias for reconfigure({ locale })\n     * @example DateTime.local(2017, 5, 25).setLocale('en-GB')\n     * @return {DateTime}\n     */\n    setLocale(locale) {\n      return this.reconfigure({ locale });\n    }\n\n    /**\n     * \"Set\" the values of specified units. Returns a newly-constructed DateTime.\n     * You can only set units with this method; for \"setting\" metadata, see {@link DateTime#reconfigure} and {@link DateTime#setZone}.\n     *\n     * This method also supports setting locale-based week units, i.e. `localWeekday`, `localWeekNumber` and `localWeekYear`.\n     * They cannot be mixed with ISO-week units like `weekday`.\n     * @param {Object} values - a mapping of units to numbers\n     * @example dt.set({ year: 2017 })\n     * @example dt.set({ hour: 8, minute: 30 })\n     * @example dt.set({ weekday: 5 })\n     * @example dt.set({ year: 2005, ordinal: 234 })\n     * @return {DateTime}\n     */\n    set(values) {\n      if (!this.isValid) return this;\n\n      const normalized = normalizeObject(values, normalizeUnitWithLocalWeeks);\n      const { minDaysInFirstWeek, startOfWeek } = usesLocalWeekValues(normalized, this.loc);\n\n      const settingWeekStuff =\n          !isUndefined(normalized.weekYear) ||\n          !isUndefined(normalized.weekNumber) ||\n          !isUndefined(normalized.weekday),\n        containsOrdinal = !isUndefined(normalized.ordinal),\n        containsGregorYear = !isUndefined(normalized.year),\n        containsGregorMD = !isUndefined(normalized.month) || !isUndefined(normalized.day),\n        containsGregor = containsGregorYear || containsGregorMD,\n        definiteWeekDef = normalized.weekYear || normalized.weekNumber;\n\n      if ((containsGregor || containsOrdinal) && definiteWeekDef) {\n        throw new ConflictingSpecificationError(\n          \"Can't mix weekYear/weekNumber units with year/month/day or ordinals\"\n        );\n      }\n\n      if (containsGregorMD && containsOrdinal) {\n        throw new ConflictingSpecificationError(\"Can't mix ordinal dates with month/day\");\n      }\n\n      let mixed;\n      if (settingWeekStuff) {\n        mixed = weekToGregorian(\n          { ...gregorianToWeek(this.c, minDaysInFirstWeek, startOfWeek), ...normalized },\n          minDaysInFirstWeek,\n          startOfWeek\n        );\n      } else if (!isUndefined(normalized.ordinal)) {\n        mixed = ordinalToGregorian({ ...gregorianToOrdinal(this.c), ...normalized });\n      } else {\n        mixed = { ...this.toObject(), ...normalized };\n\n        // if we didn't set the day but we ended up on an overflow date,\n        // use the last day of the right month\n        if (isUndefined(normalized.day)) {\n          mixed.day = Math.min(daysInMonth(mixed.year, mixed.month), mixed.day);\n        }\n      }\n\n      const [ts, o] = objToTS(mixed, this.o, this.zone);\n      return clone(this, { ts, o });\n    }\n\n    /**\n     * Add a period of time to this DateTime and return the resulting DateTime\n     *\n     * Adding hours, minutes, seconds, or milliseconds increases the timestamp by the right number of milliseconds. Adding days, months, or years shifts the calendar, accounting for DSTs and leap years along the way. Thus, `dt.plus({ hours: 24 })` may result in a different time than `dt.plus({ days: 1 })` if there's a DST shift in between.\n     * @param {Duration|Object|number} duration - The amount to add. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject()\n     * @example DateTime.now().plus(123) //~> in 123 milliseconds\n     * @example DateTime.now().plus({ minutes: 15 }) //~> in 15 minutes\n     * @example DateTime.now().plus({ days: 1 }) //~> this time tomorrow\n     * @example DateTime.now().plus({ days: -1 }) //~> this time yesterday\n     * @example DateTime.now().plus({ hours: 3, minutes: 13 }) //~> in 3 hr, 13 min\n     * @example DateTime.now().plus(Duration.fromObject({ hours: 3, minutes: 13 })) //~> in 3 hr, 13 min\n     * @return {DateTime}\n     */\n    plus(duration) {\n      if (!this.isValid) return this;\n      const dur = Duration.fromDurationLike(duration);\n      return clone(this, adjustTime(this, dur));\n    }\n\n    /**\n     * Subtract a period of time to this DateTime and return the resulting DateTime\n     * See {@link DateTime#plus}\n     * @param {Duration|Object|number} duration - The amount to subtract. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject()\n     @return {DateTime}\n     */\n    minus(duration) {\n      if (!this.isValid) return this;\n      const dur = Duration.fromDurationLike(duration).negate();\n      return clone(this, adjustTime(this, dur));\n    }\n\n    /**\n     * \"Set\" this DateTime to the beginning of a unit of time.\n     * @param {string} unit - The unit to go to the beginning of. Can be 'year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second', or 'millisecond'.\n     * @param {Object} opts - options\n     * @param {boolean} [opts.useLocaleWeeks=false] - If true, use weeks based on the locale, i.e. use the locale-dependent start of the week\n     * @example DateTime.local(2014, 3, 3).startOf('month').toISODate(); //=> '2014-03-01'\n     * @example DateTime.local(2014, 3, 3).startOf('year').toISODate(); //=> '2014-01-01'\n     * @example DateTime.local(2014, 3, 3).startOf('week').toISODate(); //=> '2014-03-03', weeks always start on Mondays\n     * @example DateTime.local(2014, 3, 3, 5, 30).startOf('day').toISOTime(); //=> '00:00.000-05:00'\n     * @example DateTime.local(2014, 3, 3, 5, 30).startOf('hour').toISOTime(); //=> '05:00:00.000-05:00'\n     * @return {DateTime}\n     */\n    startOf(unit, { useLocaleWeeks = false } = {}) {\n      if (!this.isValid) return this;\n\n      const o = {},\n        normalizedUnit = Duration.normalizeUnit(unit);\n      switch (normalizedUnit) {\n        case \"years\":\n          o.month = 1;\n        // falls through\n        case \"quarters\":\n        case \"months\":\n          o.day = 1;\n        // falls through\n        case \"weeks\":\n        case \"days\":\n          o.hour = 0;\n        // falls through\n        case \"hours\":\n          o.minute = 0;\n        // falls through\n        case \"minutes\":\n          o.second = 0;\n        // falls through\n        case \"seconds\":\n          o.millisecond = 0;\n          break;\n        // no default, invalid units throw in normalizeUnit()\n      }\n\n      if (normalizedUnit === \"weeks\") {\n        if (useLocaleWeeks) {\n          const startOfWeek = this.loc.getStartOfWeek();\n          const { weekday } = this;\n          if (weekday < startOfWeek) {\n            o.weekNumber = this.weekNumber - 1;\n          }\n          o.weekday = startOfWeek;\n        } else {\n          o.weekday = 1;\n        }\n      }\n\n      if (normalizedUnit === \"quarters\") {\n        const q = Math.ceil(this.month / 3);\n        o.month = (q - 1) * 3 + 1;\n      }\n\n      return this.set(o);\n    }\n\n    /**\n     * \"Set\" this DateTime to the end (meaning the last millisecond) of a unit of time\n     * @param {string} unit - The unit to go to the end of. Can be 'year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second', or 'millisecond'.\n     * @param {Object} opts - options\n     * @param {boolean} [opts.useLocaleWeeks=false] - If true, use weeks based on the locale, i.e. use the locale-dependent start of the week\n     * @example DateTime.local(2014, 3, 3).endOf('month').toISO(); //=> '2014-03-31T23:59:59.999-05:00'\n     * @example DateTime.local(2014, 3, 3).endOf('year').toISO(); //=> '2014-12-31T23:59:59.999-05:00'\n     * @example DateTime.local(2014, 3, 3).endOf('week').toISO(); // => '2014-03-09T23:59:59.999-05:00', weeks start on Mondays\n     * @example DateTime.local(2014, 3, 3, 5, 30).endOf('day').toISO(); //=> '2014-03-03T23:59:59.999-05:00'\n     * @example DateTime.local(2014, 3, 3, 5, 30).endOf('hour').toISO(); //=> '2014-03-03T05:59:59.999-05:00'\n     * @return {DateTime}\n     */\n    endOf(unit, opts) {\n      return this.isValid\n        ? this.plus({ [unit]: 1 })\n            .startOf(unit, opts)\n            .minus(1)\n        : this;\n    }\n\n    // OUTPUT\n\n    /**\n     * Returns a string representation of this DateTime formatted according to the specified format string.\n     * **You may not want this.** See {@link DateTime#toLocaleString} for a more flexible formatting tool. For a table of tokens and their interpretations, see [here](https://moment.github.io/luxon/#/formatting?id=table-of-tokens).\n     * Defaults to en-US if no locale has been specified, regardless of the system's locale.\n     * @param {string} fmt - the format string\n     * @param {Object} opts - opts to override the configuration options on this DateTime\n     * @example DateTime.now().toFormat('yyyy LLL dd') //=> '2017 Apr 22'\n     * @example DateTime.now().setLocale('fr').toFormat('yyyy LLL dd') //=> '2017 avr. 22'\n     * @example DateTime.now().toFormat('yyyy LLL dd', { locale: \"fr\" }) //=> '2017 avr. 22'\n     * @example DateTime.now().toFormat(\"HH 'hours and' mm 'minutes'\") //=> '20 hours and 55 minutes'\n     * @return {string}\n     */\n    toFormat(fmt, opts = {}) {\n      return this.isValid\n        ? Formatter.create(this.loc.redefaultToEN(opts)).formatDateTimeFromString(this, fmt)\n        : INVALID;\n    }\n\n    /**\n     * Returns a localized string representing this date. Accepts the same options as the Intl.DateTimeFormat constructor and any presets defined by Luxon, such as `DateTime.DATE_FULL` or `DateTime.TIME_SIMPLE`.\n     * The exact behavior of this method is browser-specific, but in general it will return an appropriate representation\n     * of the DateTime in the assigned locale.\n     * Defaults to the system's locale if no locale has been specified\n     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat\n     * @param formatOpts {Object} - Intl.DateTimeFormat constructor options and configuration options\n     * @param {Object} opts - opts to override the configuration options on this DateTime\n     * @example DateTime.now().toLocaleString(); //=> 4/20/2017\n     * @example DateTime.now().setLocale('en-gb').toLocaleString(); //=> '20/04/2017'\n     * @example DateTime.now().toLocaleString(DateTime.DATE_FULL); //=> 'April 20, 2017'\n     * @example DateTime.now().toLocaleString(DateTime.DATE_FULL, { locale: 'fr' }); //=> '28 ao\u00fbt 2022'\n     * @example DateTime.now().toLocaleString(DateTime.TIME_SIMPLE); //=> '11:32 AM'\n     * @example DateTime.now().toLocaleString(DateTime.DATETIME_SHORT); //=> '4/20/2017, 11:32 AM'\n     * @example DateTime.now().toLocaleString({ weekday: 'long', month: 'long', day: '2-digit' }); //=> 'Thursday, April 20'\n     * @example DateTime.now().toLocaleString({ weekday: 'short', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); //=> 'Thu, Apr 20, 11:27 AM'\n     * @example DateTime.now().toLocaleString({ hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }); //=> '11:32'\n     * @return {string}\n     */\n    toLocaleString(formatOpts = DATE_SHORT, opts = {}) {\n      return this.isValid\n        ? Formatter.create(this.loc.clone(opts), formatOpts).formatDateTime(this)\n        : INVALID;\n    }\n\n    /**\n     * Returns an array of format \"parts\", meaning individual tokens along with metadata. This is allows callers to post-process individual sections of the formatted output.\n     * Defaults to the system's locale if no locale has been specified\n     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/formatToParts\n     * @param opts {Object} - Intl.DateTimeFormat constructor options, same as `toLocaleString`.\n     * @example DateTime.now().toLocaleParts(); //=> [\n     *                                   //=>   { type: 'day', value: '25' },\n     *                                   //=>   { type: 'literal', value: '/' },\n     *                                   //=>   { type: 'month', value: '05' },\n     *                                   //=>   { type: 'literal', value: '/' },\n     *                                   //=>   { type: 'year', value: '1982' }\n     *                                   //=> ]\n     */\n    toLocaleParts(opts = {}) {\n      return this.isValid\n        ? Formatter.create(this.loc.clone(opts), opts).formatDateTimeParts(this)\n        : [];\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of this DateTime\n     * @param {Object} opts - options\n     * @param {boolean} [opts.suppressMilliseconds=false] - exclude milliseconds from the format if they're 0\n     * @param {boolean} [opts.suppressSeconds=false] - exclude seconds from the format if they're 0\n     * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00'\n     * @param {boolean} [opts.extendedZone=false] - add the time zone format extension\n     * @param {string} [opts.format='extended'] - choose between the basic and extended format\n     * @example DateTime.utc(1983, 5, 25).toISO() //=> '1982-05-25T00:00:00.000Z'\n     * @example DateTime.now().toISO() //=> '2017-04-22T20:47:05.335-04:00'\n     * @example DateTime.now().toISO({ includeOffset: false }) //=> '2017-04-22T20:47:05.335'\n     * @example DateTime.now().toISO({ format: 'basic' }) //=> '20170422T204705.335-0400'\n     * @return {string}\n     */\n    toISO({\n      format = \"extended\",\n      suppressSeconds = false,\n      suppressMilliseconds = false,\n      includeOffset = true,\n      extendedZone = false,\n    } = {}) {\n      if (!this.isValid) {\n        return null;\n      }\n\n      const ext = format === \"extended\";\n\n      let c = toISODate(this, ext);\n      c += \"T\";\n      c += toISOTime(this, ext, suppressSeconds, suppressMilliseconds, includeOffset, extendedZone);\n      return c;\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of this DateTime's date component\n     * @param {Object} opts - options\n     * @param {string} [opts.format='extended'] - choose between the basic and extended format\n     * @example DateTime.utc(1982, 5, 25).toISODate() //=> '1982-05-25'\n     * @example DateTime.utc(1982, 5, 25).toISODate({ format: 'basic' }) //=> '19820525'\n     * @return {string}\n     */\n    toISODate({ format = \"extended\" } = {}) {\n      if (!this.isValid) {\n        return null;\n      }\n\n      return toISODate(this, format === \"extended\");\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of this DateTime's week date\n     * @example DateTime.utc(1982, 5, 25).toISOWeekDate() //=> '1982-W21-2'\n     * @return {string}\n     */\n    toISOWeekDate() {\n      return toTechFormat(this, \"kkkk-'W'WW-c\");\n    }\n\n    /**\n     * Returns an ISO 8601-compliant string representation of this DateTime's time component\n     * @param {Object} opts - options\n     * @param {boolean} [opts.suppressMilliseconds=false] - exclude milliseconds from the format if they're 0\n     * @param {boolean} [opts.suppressSeconds=false] - exclude seconds from the format if they're 0\n     * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00'\n     * @param {boolean} [opts.extendedZone=true] - add the time zone format extension\n     * @param {boolean} [opts.includePrefix=false] - include the `T` prefix\n     * @param {string} [opts.format='extended'] - choose between the basic and extended format\n     * @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime() //=> '07:34:19.361Z'\n     * @example DateTime.utc().set({ hour: 7, minute: 34, seconds: 0, milliseconds: 0 }).toISOTime({ suppressSeconds: true }) //=> '07:34Z'\n     * @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime({ format: 'basic' }) //=> '073419.361Z'\n     * @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime({ includePrefix: true }) //=> 'T07:34:19.361Z'\n     * @return {string}\n     */\n    toISOTime({\n      suppressMilliseconds = false,\n      suppressSeconds = false,\n      includeOffset = true,\n      includePrefix = false,\n      extendedZone = false,\n      format = \"extended\",\n    } = {}) {\n      if (!this.isValid) {\n        return null;\n      }\n\n      let c = includePrefix ? \"T\" : \"\";\n      return (\n        c +\n        toISOTime(\n          this,\n          format === \"extended\",\n          suppressSeconds,\n          suppressMilliseconds,\n          includeOffset,\n          extendedZone\n        )\n      );\n    }\n\n    /**\n     * Returns an RFC 2822-compatible string representation of this DateTime\n     * @example DateTime.utc(2014, 7, 13).toRFC2822() //=> 'Sun, 13 Jul 2014 00:00:00 +0000'\n     * @example DateTime.local(2014, 7, 13).toRFC2822() //=> 'Sun, 13 Jul 2014 00:00:00 -0400'\n     * @return {string}\n     */\n    toRFC2822() {\n      return toTechFormat(this, \"EEE, dd LLL yyyy HH:mm:ss ZZZ\", false);\n    }\n\n    /**\n     * Returns a string representation of this DateTime appropriate for use in HTTP headers. The output is always expressed in GMT.\n     * Specifically, the string conforms to RFC 1123.\n     * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1\n     * @example DateTime.utc(2014, 7, 13).toHTTP() //=> 'Sun, 13 Jul 2014 00:00:00 GMT'\n     * @example DateTime.utc(2014, 7, 13, 19).toHTTP() //=> 'Sun, 13 Jul 2014 19:00:00 GMT'\n     * @return {string}\n     */\n    toHTTP() {\n      return toTechFormat(this.toUTC(), \"EEE, dd LLL yyyy HH:mm:ss 'GMT'\");\n    }\n\n    /**\n     * Returns a string representation of this DateTime appropriate for use in SQL Date\n     * @example DateTime.utc(2014, 7, 13).toSQLDate() //=> '2014-07-13'\n     * @return {string}\n     */\n    toSQLDate() {\n      if (!this.isValid) {\n        return null;\n      }\n      return toISODate(this, true);\n    }\n\n    /**\n     * Returns a string representation of this DateTime appropriate for use in SQL Time\n     * @param {Object} opts - options\n     * @param {boolean} [opts.includeZone=false] - include the zone, such as 'America/New_York'. Overrides includeOffset.\n     * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00'\n     * @param {boolean} [opts.includeOffsetSpace=true] - include the space between the time and the offset, such as '05:15:16.345 -04:00'\n     * @example DateTime.utc().toSQL() //=> '05:15:16.345'\n     * @example DateTime.now().toSQL() //=> '05:15:16.345 -04:00'\n     * @example DateTime.now().toSQL({ includeOffset: false }) //=> '05:15:16.345'\n     * @example DateTime.now().toSQL({ includeZone: false }) //=> '05:15:16.345 America/New_York'\n     * @return {string}\n     */\n    toSQLTime({ includeOffset = true, includeZone = false, includeOffsetSpace = true } = {}) {\n      let fmt = \"HH:mm:ss.SSS\";\n\n      if (includeZone || includeOffset) {\n        if (includeOffsetSpace) {\n          fmt += \" \";\n        }\n        if (includeZone) {\n          fmt += \"z\";\n        } else if (includeOffset) {\n          fmt += \"ZZ\";\n        }\n      }\n\n      return toTechFormat(this, fmt, true);\n    }\n\n    /**\n     * Returns a string representation of this DateTime appropriate for use in SQL DateTime\n     * @param {Object} opts - options\n     * @param {boolean} [opts.includeZone=false] - include the zone, such as 'America/New_York'. Overrides includeOffset.\n     * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00'\n     * @param {boolean} [opts.includeOffsetSpace=true] - include the space between the time and the offset, such as '05:15:16.345 -04:00'\n     * @example DateTime.utc(2014, 7, 13).toSQL() //=> '2014-07-13 00:00:00.000 Z'\n     * @example DateTime.local(2014, 7, 13).toSQL() //=> '2014-07-13 00:00:00.000 -04:00'\n     * @example DateTime.local(2014, 7, 13).toSQL({ includeOffset: false }) //=> '2014-07-13 00:00:00.000'\n     * @example DateTime.local(2014, 7, 13).toSQL({ includeZone: true }) //=> '2014-07-13 00:00:00.000 America/New_York'\n     * @return {string}\n     */\n    toSQL(opts = {}) {\n      if (!this.isValid) {\n        return null;\n      }\n\n      return `${this.toSQLDate()} ${this.toSQLTime(opts)}`;\n    }\n\n    /**\n     * Returns a string representation of this DateTime appropriate for debugging\n     * @return {string}\n     */\n    toString() {\n      return this.isValid ? this.toISO() : INVALID;\n    }\n\n    /**\n     * Returns a string representation of this DateTime appropriate for the REPL.\n     * @return {string}\n     */\n    [Symbol.for(\"nodejs.util.inspect.custom\")]() {\n      if (this.isValid) {\n        return `DateTime { ts: ${this.toISO()}, zone: ${this.zone.name}, locale: ${this.locale} }`;\n      } else {\n        return `DateTime { Invalid, reason: ${this.invalidReason} }`;\n      }\n    }\n\n    /**\n     * Returns the epoch milliseconds of this DateTime. Alias of {@link DateTime#toMillis}\n     * @return {number}\n     */\n    valueOf() {\n      return this.toMillis();\n    }\n\n    /**\n     * Returns the epoch milliseconds of this DateTime.\n     * @return {number}\n     */\n    toMillis() {\n      return this.isValid ? this.ts : NaN;\n    }\n\n    /**\n     * Returns the epoch seconds of this DateTime.\n     * @return {number}\n     */\n    toSeconds() {\n      return this.isValid ? this.ts / 1000 : NaN;\n    }\n\n    /**\n     * Returns the epoch seconds (as a whole number) of this DateTime.\n     * @return {number}\n     */\n    toUnixInteger() {\n      return this.isValid ? Math.floor(this.ts / 1000) : NaN;\n    }\n\n    /**\n     * Returns an ISO 8601 representation of this DateTime appropriate for use in JSON.\n     * @return {string}\n     */\n    toJSON() {\n      return this.toISO();\n    }\n\n    /**\n     * Returns a BSON serializable equivalent to this DateTime.\n     * @return {Date}\n     */\n    toBSON() {\n      return this.toJSDate();\n    }\n\n    /**\n     * Returns a JavaScript object with this DateTime's year, month, day, and so on.\n     * @param opts - options for generating the object\n     * @param {boolean} [opts.includeConfig=false] - include configuration attributes in the output\n     * @example DateTime.now().toObject() //=> { year: 2017, month: 4, day: 22, hour: 20, minute: 49, second: 42, millisecond: 268 }\n     * @return {Object}\n     */\n    toObject(opts = {}) {\n      if (!this.isValid) return {};\n\n      const base = { ...this.c };\n\n      if (opts.includeConfig) {\n        base.outputCalendar = this.outputCalendar;\n        base.numberingSystem = this.loc.numberingSystem;\n        base.locale = this.loc.locale;\n      }\n      return base;\n    }\n\n    /**\n     * Returns a JavaScript Date equivalent to this DateTime.\n     * @return {Date}\n     */\n    toJSDate() {\n      return new Date(this.isValid ? this.ts : NaN);\n    }\n\n    // COMPARE\n\n    /**\n     * Return the difference between two DateTimes as a Duration.\n     * @param {DateTime} otherDateTime - the DateTime to compare this one to\n     * @param {string|string[]} [unit=['milliseconds']] - the unit or array of units (such as 'hours' or 'days') to include in the duration.\n     * @param {Object} opts - options that affect the creation of the Duration\n     * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use\n     * @example\n     * var i1 = DateTime.fromISO('1982-05-25T09:45'),\n     *     i2 = DateTime.fromISO('1983-10-14T10:30');\n     * i2.diff(i1).toObject() //=> { milliseconds: 43807500000 }\n     * i2.diff(i1, 'hours').toObject() //=> { hours: 12168.75 }\n     * i2.diff(i1, ['months', 'days']).toObject() //=> { months: 16, days: 19.03125 }\n     * i2.diff(i1, ['months', 'days', 'hours']).toObject() //=> { months: 16, days: 19, hours: 0.75 }\n     * @return {Duration}\n     */\n    diff(otherDateTime, unit = \"milliseconds\", opts = {}) {\n      if (!this.isValid || !otherDateTime.isValid) {\n        return Duration.invalid(\"created by diffing an invalid DateTime\");\n      }\n\n      const durOpts = { locale: this.locale, numberingSystem: this.numberingSystem, ...opts };\n\n      const units = maybeArray(unit).map(Duration.normalizeUnit),\n        otherIsLater = otherDateTime.valueOf() > this.valueOf(),\n        earlier = otherIsLater ? this : otherDateTime,\n        later = otherIsLater ? otherDateTime : this,\n        diffed = diff(earlier, later, units, durOpts);\n\n      return otherIsLater ? diffed.negate() : diffed;\n    }\n\n    /**\n     * Return the difference between this DateTime and right now.\n     * See {@link DateTime#diff}\n     * @param {string|string[]} [unit=['milliseconds']] - the unit or units units (such as 'hours' or 'days') to include in the duration\n     * @param {Object} opts - options that affect the creation of the Duration\n     * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use\n     * @return {Duration}\n     */\n    diffNow(unit = \"milliseconds\", opts = {}) {\n      return this.diff(DateTime.now(), unit, opts);\n    }\n\n    /**\n     * Return an Interval spanning between this DateTime and another DateTime\n     * @param {DateTime} otherDateTime - the other end point of the Interval\n     * @return {Interval}\n     */\n    until(otherDateTime) {\n      return this.isValid ? Interval.fromDateTimes(this, otherDateTime) : this;\n    }\n\n    /**\n     * Return whether this DateTime is in the same unit of time as another DateTime.\n     * Higher-order units must also be identical for this function to return `true`.\n     * Note that time zones are **ignored** in this comparison, which compares the **local** calendar time. Use {@link DateTime#setZone} to convert one of the dates if needed.\n     * @param {DateTime} otherDateTime - the other DateTime\n     * @param {string} unit - the unit of time to check sameness on\n     * @param {Object} opts - options\n     * @param {boolean} [opts.useLocaleWeeks=false] - If true, use weeks based on the locale, i.e. use the locale-dependent start of the week; only the locale of this DateTime is used\n     * @example DateTime.now().hasSame(otherDT, 'day'); //~> true if otherDT is in the same current calendar day\n     * @return {boolean}\n     */\n    hasSame(otherDateTime, unit, opts) {\n      if (!this.isValid) return false;\n\n      const inputMs = otherDateTime.valueOf();\n      const adjustedToZone = this.setZone(otherDateTime.zone, { keepLocalTime: true });\n      return (\n        adjustedToZone.startOf(unit, opts) <= inputMs && inputMs <= adjustedToZone.endOf(unit, opts)\n      );\n    }\n\n    /**\n     * Equality check\n     * Two DateTimes are equal if and only if they represent the same millisecond, have the same zone and location, and are both valid.\n     * To compare just the millisecond values, use `+dt1 === +dt2`.\n     * @param {DateTime} other - the other DateTime\n     * @return {boolean}\n     */\n    equals(other) {\n      return (\n        this.isValid &&\n        other.isValid &&\n        this.valueOf() === other.valueOf() &&\n        this.zone.equals(other.zone) &&\n        this.loc.equals(other.loc)\n      );\n    }\n\n    /**\n     * Returns a string representation of a this time relative to now, such as \"in two days\". Can only internationalize if your\n     * platform supports Intl.RelativeTimeFormat. Rounds down by default.\n     * @param {Object} options - options that affect the output\n     * @param {DateTime} [options.base=DateTime.now()] - the DateTime to use as the basis to which this time is compared. Defaults to now.\n     * @param {string} [options.style=\"long\"] - the style of units, must be \"long\", \"short\", or \"narrow\"\n     * @param {string|string[]} options.unit - use a specific unit or array of units; if omitted, or an array, the method will pick the best unit. Use an array or one of \"years\", \"quarters\", \"months\", \"weeks\", \"days\", \"hours\", \"minutes\", or \"seconds\"\n     * @param {boolean} [options.round=true] - whether to round the numbers in the output.\n     * @param {number} [options.padding=0] - padding in milliseconds. This allows you to round up the result if it fits inside the threshold. Don't use in combination with {round: false} because the decimal output will include the padding.\n     * @param {string} options.locale - override the locale of this DateTime\n     * @param {string} options.numberingSystem - override the numberingSystem of this DateTime. The Intl system may choose not to honor this\n     * @example DateTime.now().plus({ days: 1 }).toRelative() //=> \"in 1 day\"\n     * @example DateTime.now().setLocale(\"es\").toRelative({ days: 1 }) //=> \"dentro de 1 d\u00eda\"\n     * @example DateTime.now().plus({ days: 1 }).toRelative({ locale: \"fr\" }) //=> \"dans 23 heures\"\n     * @example DateTime.now().minus({ days: 2 }).toRelative() //=> \"2 days ago\"\n     * @example DateTime.now().minus({ days: 2 }).toRelative({ unit: \"hours\" }) //=> \"48 hours ago\"\n     * @example DateTime.now().minus({ hours: 36 }).toRelative({ round: false }) //=> \"1.5 days ago\"\n     */\n    toRelative(options = {}) {\n      if (!this.isValid) return null;\n      const base = options.base || DateTime.fromObject({}, { zone: this.zone }),\n        padding = options.padding ? (this < base ? -options.padding : options.padding) : 0;\n      let units = [\"years\", \"months\", \"days\", \"hours\", \"minutes\", \"seconds\"];\n      let unit = options.unit;\n      if (Array.isArray(options.unit)) {\n        units = options.unit;\n        unit = undefined;\n      }\n      return diffRelative(base, this.plus(padding), {\n        ...options,\n        numeric: \"always\",\n        units,\n        unit,\n      });\n    }\n\n    /**\n     * Returns a string representation of this date relative to today, such as \"yesterday\" or \"next month\".\n     * Only internationalizes on platforms that supports Intl.RelativeTimeFormat.\n     * @param {Object} options - options that affect the output\n     * @param {DateTime} [options.base=DateTime.now()] - the DateTime to use as the basis to which this time is compared. Defaults to now.\n     * @param {string} options.locale - override the locale of this DateTime\n     * @param {string} options.unit - use a specific unit; if omitted, the method will pick the unit. Use one of \"years\", \"quarters\", \"months\", \"weeks\", or \"days\"\n     * @param {string} options.numberingSystem - override the numberingSystem of this DateTime. The Intl system may choose not to honor this\n     * @example DateTime.now().plus({ days: 1 }).toRelativeCalendar() //=> \"tomorrow\"\n     * @example DateTime.now().setLocale(\"es\").plus({ days: 1 }).toRelative() //=> \"\"ma\u00f1ana\"\n     * @example DateTime.now().plus({ days: 1 }).toRelativeCalendar({ locale: \"fr\" }) //=> \"demain\"\n     * @example DateTime.now().minus({ days: 2 }).toRelativeCalendar() //=> \"2 days ago\"\n     */\n    toRelativeCalendar(options = {}) {\n      if (!this.isValid) return null;\n\n      return diffRelative(options.base || DateTime.fromObject({}, { zone: this.zone }), this, {\n        ...options,\n        numeric: \"auto\",\n        units: [\"years\", \"months\", \"days\"],\n        calendary: true,\n      });\n    }\n\n    /**\n     * Return the min of several date times\n     * @param {...DateTime} dateTimes - the DateTimes from which to choose the minimum\n     * @return {DateTime} the min DateTime, or undefined if called with no argument\n     */\n    static min(...dateTimes) {\n      if (!dateTimes.every(DateTime.isDateTime)) {\n        throw new InvalidArgumentError(\"min requires all arguments be DateTimes\");\n      }\n      return bestBy(dateTimes, (i) => i.valueOf(), Math.min);\n    }\n\n    /**\n     * Return the max of several date times\n     * @param {...DateTime} dateTimes - the DateTimes from which to choose the maximum\n     * @return {DateTime} the max DateTime, or undefined if called with no argument\n     */\n    static max(...dateTimes) {\n      if (!dateTimes.every(DateTime.isDateTime)) {\n        throw new InvalidArgumentError(\"max requires all arguments be DateTimes\");\n      }\n      return bestBy(dateTimes, (i) => i.valueOf(), Math.max);\n    }\n\n    // MISC\n\n    /**\n     * Explain how a string would be parsed by fromFormat()\n     * @param {string} text - the string to parse\n     * @param {string} fmt - the format the string is expected to be in (see description)\n     * @param {Object} options - options taken by fromFormat()\n     * @return {Object}\n     */\n    static fromFormatExplain(text, fmt, options = {}) {\n      const { locale = null, numberingSystem = null } = options,\n        localeToUse = Locale.fromOpts({\n          locale,\n          numberingSystem,\n          defaultToEN: true,\n        });\n      return explainFromTokens(localeToUse, text, fmt);\n    }\n\n    /**\n     * @deprecated use fromFormatExplain instead\n     */\n    static fromStringExplain(text, fmt, options = {}) {\n      return DateTime.fromFormatExplain(text, fmt, options);\n    }\n\n    // FORMAT PRESETS\n\n    /**\n     * {@link DateTime#toLocaleString} format like 10/14/1983\n     * @type {Object}\n     */\n    static get DATE_SHORT() {\n      return DATE_SHORT;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like 'Oct 14, 1983'\n     * @type {Object}\n     */\n    static get DATE_MED() {\n      return DATE_MED;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like 'Fri, Oct 14, 1983'\n     * @type {Object}\n     */\n    static get DATE_MED_WITH_WEEKDAY() {\n      return DATE_MED_WITH_WEEKDAY;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like 'October 14, 1983'\n     * @type {Object}\n     */\n    static get DATE_FULL() {\n      return DATE_FULL;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like 'Tuesday, October 14, 1983'\n     * @type {Object}\n     */\n    static get DATE_HUGE() {\n      return DATE_HUGE;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like '09:30 AM'. Only 12-hour if the locale is.\n     * @type {Object}\n     */\n    static get TIME_SIMPLE() {\n      return TIME_SIMPLE;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like '09:30:23 AM'. Only 12-hour if the locale is.\n     * @type {Object}\n     */\n    static get TIME_WITH_SECONDS() {\n      return TIME_WITH_SECONDS;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like '09:30:23 AM EDT'. Only 12-hour if the locale is.\n     * @type {Object}\n     */\n    static get TIME_WITH_SHORT_OFFSET() {\n      return TIME_WITH_SHORT_OFFSET;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like '09:30:23 AM Eastern Daylight Time'. Only 12-hour if the locale is.\n     * @type {Object}\n     */\n    static get TIME_WITH_LONG_OFFSET() {\n      return TIME_WITH_LONG_OFFSET;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like '09:30', always 24-hour.\n     * @type {Object}\n     */\n    static get TIME_24_SIMPLE() {\n      return TIME_24_SIMPLE;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like '09:30:23', always 24-hour.\n     * @type {Object}\n     */\n    static get TIME_24_WITH_SECONDS() {\n      return TIME_24_WITH_SECONDS;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like '09:30:23 EDT', always 24-hour.\n     * @type {Object}\n     */\n    static get TIME_24_WITH_SHORT_OFFSET() {\n      return TIME_24_WITH_SHORT_OFFSET;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like '09:30:23 Eastern Daylight Time', always 24-hour.\n     * @type {Object}\n     */\n    static get TIME_24_WITH_LONG_OFFSET() {\n      return TIME_24_WITH_LONG_OFFSET;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like '10/14/1983, 9:30 AM'. Only 12-hour if the locale is.\n     * @type {Object}\n     */\n    static get DATETIME_SHORT() {\n      return DATETIME_SHORT;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like '10/14/1983, 9:30:33 AM'. Only 12-hour if the locale is.\n     * @type {Object}\n     */\n    static get DATETIME_SHORT_WITH_SECONDS() {\n      return DATETIME_SHORT_WITH_SECONDS;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like 'Oct 14, 1983, 9:30 AM'. Only 12-hour if the locale is.\n     * @type {Object}\n     */\n    static get DATETIME_MED() {\n      return DATETIME_MED;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like 'Oct 14, 1983, 9:30:33 AM'. Only 12-hour if the locale is.\n     * @type {Object}\n     */\n    static get DATETIME_MED_WITH_SECONDS() {\n      return DATETIME_MED_WITH_SECONDS;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like 'Fri, 14 Oct 1983, 9:30 AM'. Only 12-hour if the locale is.\n     * @type {Object}\n     */\n    static get DATETIME_MED_WITH_WEEKDAY() {\n      return DATETIME_MED_WITH_WEEKDAY;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like 'October 14, 1983, 9:30 AM EDT'. Only 12-hour if the locale is.\n     * @type {Object}\n     */\n    static get DATETIME_FULL() {\n      return DATETIME_FULL;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like 'October 14, 1983, 9:30:33 AM EDT'. Only 12-hour if the locale is.\n     * @type {Object}\n     */\n    static get DATETIME_FULL_WITH_SECONDS() {\n      return DATETIME_FULL_WITH_SECONDS;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like 'Friday, October 14, 1983, 9:30 AM Eastern Daylight Time'. Only 12-hour if the locale is.\n     * @type {Object}\n     */\n    static get DATETIME_HUGE() {\n      return DATETIME_HUGE;\n    }\n\n    /**\n     * {@link DateTime#toLocaleString} format like 'Friday, October 14, 1983, 9:30:33 AM Eastern Daylight Time'. Only 12-hour if the locale is.\n     * @type {Object}\n     */\n    static get DATETIME_HUGE_WITH_SECONDS() {\n      return DATETIME_HUGE_WITH_SECONDS;\n    }\n  }\n\n  /**\n   * @private\n   */\n  function friendlyDateTime(dateTimeish) {\n    if (DateTime.isDateTime(dateTimeish)) {\n      return dateTimeish;\n    } else if (dateTimeish && dateTimeish.valueOf && isNumber(dateTimeish.valueOf())) {\n      return DateTime.fromJSDate(dateTimeish);\n    } else if (dateTimeish && typeof dateTimeish === \"object\") {\n      return DateTime.fromObject(dateTimeish);\n    } else {\n      throw new InvalidArgumentError(\n        `Unknown datetime argument: ${dateTimeish}, of type ${typeof dateTimeish}`\n      );\n    }\n  }\n\n  const VERSION = \"3.4.4\";\n\n  exports.DateTime = DateTime;\n  exports.Duration = Duration;\n  exports.FixedOffsetZone = FixedOffsetZone;\n  exports.IANAZone = IANAZone;\n  exports.Info = Info;\n  exports.Interval = Interval;\n  exports.InvalidZone = InvalidZone;\n  exports.Settings = Settings;\n  exports.SystemZone = SystemZone;\n  exports.VERSION = VERSION;\n  exports.Zone = Zone;\n\n  Object.defineProperty(exports, '__esModule', { value: true });\n\n  return exports;\n\n})({});\n// start Odoo customization\n// The following prevents luxon objects from being made reactive by Owl, because they are immutable\nluxon.DateTime.prototype[Symbol.toStringTag] = \"LuxonDateTime\";\nluxon.Duration.prototype[Symbol.toStringTag] = \"LuxonDuration\";\nluxon.Interval.prototype[Symbol.toStringTag] = \"LuxonInterval\";\nluxon.Settings.prototype[Symbol.toStringTag] = \"LuxonSettings\";\nluxon.Info.prototype[Symbol.toStringTag] = \"LuxonInfo\";\nluxon.Zone.prototype[Symbol.toStringTag] = \"LuxonZone\";\n// end Odoo customization\n//# sourceMappingURL=luxon.js.map\n", "// @odoo-module ignore\nif (!Object.hasOwn) {\n    Object.hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);\n}\n", "// @odoo-module ignore\nif (!Array.prototype.at) {\n    Object.defineProperty(Array.prototype, \"at\", {\n        enumerable: false,\n        value: function (index) {\n            if (index >= 0) {\n                return this[index];\n            }\n            return this[this.length + index];\n        }\n    });\n}\n", "// @odoo-module ignore\n\n(function () {\n'use strict';\n\n/**\n * This file makes sure textarea elements with a specific editor class are\n * tweaked as soon as the DOM is ready so that they appear to be loading.\n *\n * They must then be loaded using standard Odoo modules system. In particular,\n * @see @web_editor/js/frontend/loadWysiwygFromTextarea\n */\n\ndocument.addEventListener('DOMContentLoaded', () => {\n    // Standard loop for better browser support\n    var textareaEls = document.querySelectorAll('textarea.o_wysiwyg_loader');\n    for (var i = 0; i < textareaEls.length; i++) {\n        var textarea = textareaEls[i];\n        var wrapper = document.createElement('div');\n        wrapper.classList.add('position-relative', 'o_wysiwyg_textarea_wrapper');\n\n        var loadingElement = document.createElement('div');\n        loadingElement.classList.add('o_wysiwyg_loading');\n        var loadingIcon = document.createElement('i');\n        loadingIcon.classList.add('text-600', 'text-center',\n            'fa', 'fa-circle-o-notch', 'fa-spin', 'fa-2x');\n        loadingElement.appendChild(loadingIcon);\n        wrapper.appendChild(loadingElement);\n\n        textarea.parentNode.insertBefore(wrapper, textarea);\n        wrapper.insertBefore(textarea, loadingElement);\n    }\n});\n\n})();\n", "(function (exports) {\r\n    'use strict';\r\n\r\n    function filterOutModifiersFromData(dataList) {\r\n        dataList = dataList.slice();\r\n        const modifiers = [];\r\n        let elm;\r\n        while ((elm = dataList[0]) && typeof elm === \"string\") {\r\n            modifiers.push(dataList.shift());\r\n        }\r\n        return { modifiers, data: dataList };\r\n    }\r\n    const config = {\r\n        // whether or not blockdom should normalize DOM whenever a block is created.\r\n        // Normalizing dom mean removing empty text nodes (or containing only spaces)\r\n        shouldNormalizeDom: true,\r\n        // this is the main event handler. Every event handler registered with blockdom\r\n        // will go through this function, giving it the data registered in the block\r\n        // and the event\r\n        mainEventHandler: (data, ev, currentTarget) => {\r\n            if (typeof data === \"function\") {\r\n                data(ev);\r\n            }\r\n            else if (Array.isArray(data)) {\r\n                data = filterOutModifiersFromData(data).data;\r\n                data[0](data[1], ev);\r\n            }\r\n            return false;\r\n        },\r\n    };\r\n\r\n    // -----------------------------------------------------------------------------\r\n    // Toggler node\r\n    // -----------------------------------------------------------------------------\r\n    class VToggler {\r\n        constructor(key, child) {\r\n            this.key = key;\r\n            this.child = child;\r\n        }\r\n        mount(parent, afterNode) {\r\n            this.parentEl = parent;\r\n            this.child.mount(parent, afterNode);\r\n        }\r\n        moveBeforeDOMNode(node, parent) {\r\n            this.child.moveBeforeDOMNode(node, parent);\r\n        }\r\n        moveBeforeVNode(other, afterNode) {\r\n            this.moveBeforeDOMNode((other && other.firstNode()) || afterNode);\r\n        }\r\n        patch(other, withBeforeRemove) {\r\n            if (this === other) {\r\n                return;\r\n            }\r\n            let child1 = this.child;\r\n            let child2 = other.child;\r\n            if (this.key === other.key) {\r\n                child1.patch(child2, withBeforeRemove);\r\n            }\r\n            else {\r\n                child2.mount(this.parentEl, child1.firstNode());\r\n                if (withBeforeRemove) {\r\n                    child1.beforeRemove();\r\n                }\r\n                child1.remove();\r\n                this.child = child2;\r\n                this.key = other.key;\r\n            }\r\n        }\r\n        beforeRemove() {\r\n            this.child.beforeRemove();\r\n        }\r\n        remove() {\r\n            this.child.remove();\r\n        }\r\n        firstNode() {\r\n            return this.child.firstNode();\r\n        }\r\n        toString() {\r\n            return this.child.toString();\r\n        }\r\n    }\r\n    function toggler(key, child) {\r\n        return new VToggler(key, child);\r\n    }\r\n\r\n    // Custom error class that wraps error that happen in the owl lifecycle\r\n    class OwlError extends Error {\r\n    }\r\n\r\n    const { setAttribute: elemSetAttribute, removeAttribute } = Element.prototype;\r\n    const tokenList = DOMTokenList.prototype;\r\n    const tokenListAdd = tokenList.add;\r\n    const tokenListRemove = tokenList.remove;\r\n    const isArray = Array.isArray;\r\n    const { split, trim } = String.prototype;\r\n    const wordRegexp = /\\s+/;\r\n    /**\r\n     * We regroup here all code related to updating attributes in a very loose sense:\r\n     * attributes, properties and classs are all managed by the functions in this\r\n     * file.\r\n     */\r\n    function setAttribute(key, value) {\r\n        switch (value) {\r\n            case false:\r\n            case undefined:\r\n                removeAttribute.call(this, key);\r\n                break;\r\n            case true:\r\n                elemSetAttribute.call(this, key, \"\");\r\n                break;\r\n            default:\r\n                elemSetAttribute.call(this, key, value);\r\n        }\r\n    }\r\n    function createAttrUpdater(attr) {\r\n        return function (value) {\r\n            setAttribute.call(this, attr, value);\r\n        };\r\n    }\r\n    function attrsSetter(attrs) {\r\n        if (isArray(attrs)) {\r\n            if (attrs[0] === \"class\") {\r\n                setClass.call(this, attrs[1]);\r\n            }\r\n            else {\r\n                setAttribute.call(this, attrs[0], attrs[1]);\r\n            }\r\n        }\r\n        else {\r\n            for (let k in attrs) {\r\n                if (k === \"class\") {\r\n                    setClass.call(this, attrs[k]);\r\n                }\r\n                else {\r\n                    setAttribute.call(this, k, attrs[k]);\r\n                }\r\n            }\r\n        }\r\n    }\r\n    function attrsUpdater(attrs, oldAttrs) {\r\n        if (isArray(attrs)) {\r\n            const name = attrs[0];\r\n            const val = attrs[1];\r\n            if (name === oldAttrs[0]) {\r\n                if (val === oldAttrs[1]) {\r\n                    return;\r\n                }\r\n                if (name === \"class\") {\r\n                    updateClass.call(this, val, oldAttrs[1]);\r\n                }\r\n                else {\r\n                    setAttribute.call(this, name, val);\r\n                }\r\n            }\r\n            else {\r\n                removeAttribute.call(this, oldAttrs[0]);\r\n                setAttribute.call(this, name, val);\r\n            }\r\n        }\r\n        else {\r\n            for (let k in oldAttrs) {\r\n                if (!(k in attrs)) {\r\n                    if (k === \"class\") {\r\n                        updateClass.call(this, \"\", oldAttrs[k]);\r\n                    }\r\n                    else {\r\n                        removeAttribute.call(this, k);\r\n                    }\r\n                }\r\n            }\r\n            for (let k in attrs) {\r\n                const val = attrs[k];\r\n                if (val !== oldAttrs[k]) {\r\n                    if (k === \"class\") {\r\n                        updateClass.call(this, val, oldAttrs[k]);\r\n                    }\r\n                    else {\r\n                        setAttribute.call(this, k, val);\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n    function toClassObj(expr) {\r\n        const result = {};\r\n        switch (typeof expr) {\r\n            case \"string\":\r\n                // we transform here a list of classes into an object:\r\n                //  'hey you' becomes {hey: true, you: true}\r\n                const str = trim.call(expr);\r\n                if (!str) {\r\n                    return {};\r\n                }\r\n                let words = split.call(str, wordRegexp);\r\n                for (let i = 0, l = words.length; i < l; i++) {\r\n                    result[words[i]] = true;\r\n                }\r\n                return result;\r\n            case \"object\":\r\n                // this is already an object but we may need to split keys:\r\n                // {'a': true, 'b c': true} should become {a: true, b: true, c: true}\r\n                for (let key in expr) {\r\n                    const value = expr[key];\r\n                    if (value) {\r\n                        key = trim.call(key);\r\n                        if (!key) {\r\n                            continue;\r\n                        }\r\n                        const words = split.call(key, wordRegexp);\r\n                        for (let word of words) {\r\n                            result[word] = value;\r\n                        }\r\n                    }\r\n                }\r\n                return result;\r\n            case \"undefined\":\r\n                return {};\r\n            case \"number\":\r\n                return { [expr]: true };\r\n            default:\r\n                return { [expr]: true };\r\n        }\r\n    }\r\n    function setClass(val) {\r\n        val = val === \"\" ? {} : toClassObj(val);\r\n        // add classes\r\n        const cl = this.classList;\r\n        for (let c in val) {\r\n            tokenListAdd.call(cl, c);\r\n        }\r\n    }\r\n    function updateClass(val, oldVal) {\r\n        oldVal = oldVal === \"\" ? {} : toClassObj(oldVal);\r\n        val = val === \"\" ? {} : toClassObj(val);\r\n        const cl = this.classList;\r\n        // remove classes\r\n        for (let c in oldVal) {\r\n            if (!(c in val)) {\r\n                tokenListRemove.call(cl, c);\r\n            }\r\n        }\r\n        // add classes\r\n        for (let c in val) {\r\n            if (!(c in oldVal)) {\r\n                tokenListAdd.call(cl, c);\r\n            }\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Creates a batched version of a callback so that all calls to it in the same\r\n     * microtick will only call the original callback once.\r\n     *\r\n     * @param callback the callback to batch\r\n     * @returns a batched version of the original callback\r\n     */\r\n    function batched(callback) {\r\n        let scheduled = false;\r\n        return async (...args) => {\r\n            if (!scheduled) {\r\n                scheduled = true;\r\n                await Promise.resolve();\r\n                scheduled = false;\r\n                callback(...args);\r\n            }\r\n        };\r\n    }\r\n    /**\r\n     * Determine whether the given element is contained in its ownerDocument:\r\n     * either directly or with a shadow root in between.\r\n     */\r\n    function inOwnerDocument(el) {\r\n        if (!el) {\r\n            return false;\r\n        }\r\n        if (el.ownerDocument.contains(el)) {\r\n            return true;\r\n        }\r\n        const rootNode = el.getRootNode();\r\n        return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);\r\n    }\r\n    function validateTarget(target) {\r\n        // Get the document and HTMLElement corresponding to the target to allow mounting in iframes\r\n        const document = target && target.ownerDocument;\r\n        if (document) {\r\n            const HTMLElement = document.defaultView.HTMLElement;\r\n            if (target instanceof HTMLElement || target instanceof ShadowRoot) {\r\n                if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {\r\n                    throw new OwlError(\"Cannot mount a component on a detached dom node\");\r\n                }\r\n                return;\r\n            }\r\n        }\r\n        throw new OwlError(\"Cannot mount component: the target is not a valid DOM element\");\r\n    }\r\n    class EventBus extends EventTarget {\r\n        trigger(name, payload) {\r\n            this.dispatchEvent(new CustomEvent(name, { detail: payload }));\r\n        }\r\n    }\r\n    function whenReady(fn) {\r\n        return new Promise(function (resolve) {\r\n            if (document.readyState !== \"loading\") {\r\n                resolve(true);\r\n            }\r\n            else {\r\n                document.addEventListener(\"DOMContentLoaded\", resolve, false);\r\n            }\r\n        }).then(fn || function () { });\r\n    }\r\n    async function loadFile(url) {\r\n        const result = await fetch(url);\r\n        if (!result.ok) {\r\n            throw new OwlError(\"Error while fetching xml templates\");\r\n        }\r\n        return await result.text();\r\n    }\r\n    /*\r\n     * This class just transports the fact that a string is safe\r\n     * to be injected as HTML. Overriding a JS primitive is quite painful though\r\n     * so we need to redfine toString and valueOf.\r\n     */\r\n    class Markup extends String {\r\n    }\r\n    /*\r\n     * Marks a value as safe, that is, a value that can be injected as HTML directly.\r\n     * It should be used to wrap the value passed to a t-out directive to allow a raw rendering.\r\n     */\r\n    function markup(value) {\r\n        return new Markup(value);\r\n    }\r\n\r\n    function createEventHandler(rawEvent) {\r\n        const eventName = rawEvent.split(\".\")[0];\r\n        const capture = rawEvent.includes(\".capture\");\r\n        if (rawEvent.includes(\".synthetic\")) {\r\n            return createSyntheticHandler(eventName, capture);\r\n        }\r\n        else {\r\n            return createElementHandler(eventName, capture);\r\n        }\r\n    }\r\n    // Native listener\r\n    let nextNativeEventId = 1;\r\n    function createElementHandler(evName, capture = false) {\r\n        let eventKey = `__event__${evName}_${nextNativeEventId++}`;\r\n        if (capture) {\r\n            eventKey = `${eventKey}_capture`;\r\n        }\r\n        function listener(ev) {\r\n            const currentTarget = ev.currentTarget;\r\n            if (!currentTarget || !inOwnerDocument(currentTarget))\r\n                return;\r\n            const data = currentTarget[eventKey];\r\n            if (!data)\r\n                return;\r\n            config.mainEventHandler(data, ev, currentTarget);\r\n        }\r\n        function setup(data) {\r\n            this[eventKey] = data;\r\n            this.addEventListener(evName, listener, { capture });\r\n        }\r\n        function remove() {\r\n            delete this[eventKey];\r\n            this.removeEventListener(evName, listener, { capture });\r\n        }\r\n        function update(data) {\r\n            this[eventKey] = data;\r\n        }\r\n        return { setup, update, remove };\r\n    }\r\n    // Synthetic handler: a form of event delegation that allows placing only one\r\n    // listener per event type.\r\n    let nextSyntheticEventId = 1;\r\n    function createSyntheticHandler(evName, capture = false) {\r\n        let eventKey = `__event__synthetic_${evName}`;\r\n        if (capture) {\r\n            eventKey = `${eventKey}_capture`;\r\n        }\r\n        setupSyntheticEvent(evName, eventKey, capture);\r\n        const currentId = nextSyntheticEventId++;\r\n        function setup(data) {\r\n            const _data = this[eventKey] || {};\r\n            _data[currentId] = data;\r\n            this[eventKey] = _data;\r\n        }\r\n        function remove() {\r\n            delete this[eventKey];\r\n        }\r\n        return { setup, update: setup, remove };\r\n    }\r\n    function nativeToSyntheticEvent(eventKey, event) {\r\n        let dom = event.target;\r\n        while (dom !== null) {\r\n            const _data = dom[eventKey];\r\n            if (_data) {\r\n                for (const data of Object.values(_data)) {\r\n                    const stopped = config.mainEventHandler(data, event, dom);\r\n                    if (stopped)\r\n                        return;\r\n                }\r\n            }\r\n            dom = dom.parentNode;\r\n        }\r\n    }\r\n    const CONFIGURED_SYNTHETIC_EVENTS = {};\r\n    function setupSyntheticEvent(evName, eventKey, capture = false) {\r\n        if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) {\r\n            return;\r\n        }\r\n        document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event), {\r\n            capture,\r\n        });\r\n        CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true;\r\n    }\r\n\r\n    const getDescriptor$3 = (o, p) => Object.getOwnPropertyDescriptor(o, p);\r\n    const nodeProto$4 = Node.prototype;\r\n    const nodeInsertBefore$3 = nodeProto$4.insertBefore;\r\n    const nodeSetTextContent$1 = getDescriptor$3(nodeProto$4, \"textContent\").set;\r\n    const nodeRemoveChild$3 = nodeProto$4.removeChild;\r\n    // -----------------------------------------------------------------------------\r\n    // Multi NODE\r\n    // -----------------------------------------------------------------------------\r\n    class VMulti {\r\n        constructor(children) {\r\n            this.children = children;\r\n        }\r\n        mount(parent, afterNode) {\r\n            const children = this.children;\r\n            const l = children.length;\r\n            const anchors = new Array(l);\r\n            for (let i = 0; i < l; i++) {\r\n                let child = children[i];\r\n                if (child) {\r\n                    child.mount(parent, afterNode);\r\n                }\r\n                else {\r\n                    const childAnchor = document.createTextNode(\"\");\r\n                    anchors[i] = childAnchor;\r\n                    nodeInsertBefore$3.call(parent, childAnchor, afterNode);\r\n                }\r\n            }\r\n            this.anchors = anchors;\r\n            this.parentEl = parent;\r\n        }\r\n        moveBeforeDOMNode(node, parent = this.parentEl) {\r\n            this.parentEl = parent;\r\n            const children = this.children;\r\n            const anchors = this.anchors;\r\n            for (let i = 0, l = children.length; i < l; i++) {\r\n                let child = children[i];\r\n                if (child) {\r\n                    child.moveBeforeDOMNode(node, parent);\r\n                }\r\n                else {\r\n                    const anchor = anchors[i];\r\n                    nodeInsertBefore$3.call(parent, anchor, node);\r\n                }\r\n            }\r\n        }\r\n        moveBeforeVNode(other, afterNode) {\r\n            if (other) {\r\n                const next = other.children[0];\r\n                afterNode = (next ? next.firstNode() : other.anchors[0]) || null;\r\n            }\r\n            const children = this.children;\r\n            const parent = this.parentEl;\r\n            const anchors = this.anchors;\r\n            for (let i = 0, l = children.length; i < l; i++) {\r\n                let child = children[i];\r\n                if (child) {\r\n                    child.moveBeforeVNode(null, afterNode);\r\n                }\r\n                else {\r\n                    const anchor = anchors[i];\r\n                    nodeInsertBefore$3.call(parent, anchor, afterNode);\r\n                }\r\n            }\r\n        }\r\n        patch(other, withBeforeRemove) {\r\n            if (this === other) {\r\n                return;\r\n            }\r\n            const children1 = this.children;\r\n            const children2 = other.children;\r\n            const anchors = this.anchors;\r\n            const parentEl = this.parentEl;\r\n            for (let i = 0, l = children1.length; i < l; i++) {\r\n                const vn1 = children1[i];\r\n                const vn2 = children2[i];\r\n                if (vn1) {\r\n                    if (vn2) {\r\n                        vn1.patch(vn2, withBeforeRemove);\r\n                    }\r\n                    else {\r\n                        const afterNode = vn1.firstNode();\r\n                        const anchor = document.createTextNode(\"\");\r\n                        anchors[i] = anchor;\r\n                        nodeInsertBefore$3.call(parentEl, anchor, afterNode);\r\n                        if (withBeforeRemove) {\r\n                            vn1.beforeRemove();\r\n                        }\r\n                        vn1.remove();\r\n                        children1[i] = undefined;\r\n                    }\r\n                }\r\n                else if (vn2) {\r\n                    children1[i] = vn2;\r\n                    const anchor = anchors[i];\r\n                    vn2.mount(parentEl, anchor);\r\n                    nodeRemoveChild$3.call(parentEl, anchor);\r\n                }\r\n            }\r\n        }\r\n        beforeRemove() {\r\n            const children = this.children;\r\n            for (let i = 0, l = children.length; i < l; i++) {\r\n                const child = children[i];\r\n                if (child) {\r\n                    child.beforeRemove();\r\n                }\r\n            }\r\n        }\r\n        remove() {\r\n            const parentEl = this.parentEl;\r\n            if (this.isOnlyChild) {\r\n                nodeSetTextContent$1.call(parentEl, \"\");\r\n            }\r\n            else {\r\n                const children = this.children;\r\n                const anchors = this.anchors;\r\n                for (let i = 0, l = children.length; i < l; i++) {\r\n                    const child = children[i];\r\n                    if (child) {\r\n                        child.remove();\r\n                    }\r\n                    else {\r\n                        nodeRemoveChild$3.call(parentEl, anchors[i]);\r\n                    }\r\n                }\r\n            }\r\n        }\r\n        firstNode() {\r\n            const child = this.children[0];\r\n            return child ? child.firstNode() : this.anchors[0];\r\n        }\r\n        toString() {\r\n            return this.children.map((c) => (c ? c.toString() : \"\")).join(\"\");\r\n        }\r\n    }\r\n    function multi(children) {\r\n        return new VMulti(children);\r\n    }\r\n\r\n    const getDescriptor$2 = (o, p) => Object.getOwnPropertyDescriptor(o, p);\r\n    const nodeProto$3 = Node.prototype;\r\n    const characterDataProto$1 = CharacterData.prototype;\r\n    const nodeInsertBefore$2 = nodeProto$3.insertBefore;\r\n    const characterDataSetData$1 = getDescriptor$2(characterDataProto$1, \"data\").set;\r\n    const nodeRemoveChild$2 = nodeProto$3.removeChild;\r\n    class VSimpleNode {\r\n        constructor(text) {\r\n            this.text = text;\r\n        }\r\n        mountNode(node, parent, afterNode) {\r\n            this.parentEl = parent;\r\n            nodeInsertBefore$2.call(parent, node, afterNode);\r\n            this.el = node;\r\n        }\r\n        moveBeforeDOMNode(node, parent = this.parentEl) {\r\n            this.parentEl = parent;\r\n            nodeInsertBefore$2.call(parent, this.el, node);\r\n        }\r\n        moveBeforeVNode(other, afterNode) {\r\n            nodeInsertBefore$2.call(this.parentEl, this.el, other ? other.el : afterNode);\r\n        }\r\n        beforeRemove() { }\r\n        remove() {\r\n            nodeRemoveChild$2.call(this.parentEl, this.el);\r\n        }\r\n        firstNode() {\r\n            return this.el;\r\n        }\r\n        toString() {\r\n            return this.text;\r\n        }\r\n    }\r\n    class VText$1 extends VSimpleNode {\r\n        mount(parent, afterNode) {\r\n            this.mountNode(document.createTextNode(toText(this.text)), parent, afterNode);\r\n        }\r\n        patch(other) {\r\n            const text2 = other.text;\r\n            if (this.text !== text2) {\r\n                characterDataSetData$1.call(this.el, toText(text2));\r\n                this.text = text2;\r\n            }\r\n        }\r\n    }\r\n    class VComment extends VSimpleNode {\r\n        mount(parent, afterNode) {\r\n            this.mountNode(document.createComment(toText(this.text)), parent, afterNode);\r\n        }\r\n        patch() { }\r\n    }\r\n    function text(str) {\r\n        return new VText$1(str);\r\n    }\r\n    function comment(str) {\r\n        return new VComment(str);\r\n    }\r\n    function toText(value) {\r\n        switch (typeof value) {\r\n            case \"string\":\r\n                return value;\r\n            case \"number\":\r\n                return String(value);\r\n            case \"boolean\":\r\n                return value ? \"true\" : \"false\";\r\n            default:\r\n                return value || \"\";\r\n        }\r\n    }\r\n\r\n    const getDescriptor$1 = (o, p) => Object.getOwnPropertyDescriptor(o, p);\r\n    const nodeProto$2 = Node.prototype;\r\n    const elementProto = Element.prototype;\r\n    const characterDataProto = CharacterData.prototype;\r\n    const characterDataSetData = getDescriptor$1(characterDataProto, \"data\").set;\r\n    const nodeGetFirstChild = getDescriptor$1(nodeProto$2, \"firstChild\").get;\r\n    const nodeGetNextSibling = getDescriptor$1(nodeProto$2, \"nextSibling\").get;\r\n    const NO_OP = () => { };\r\n    function makePropSetter(name) {\r\n        return function setProp(value) {\r\n            // support 0, fallback to empty string for other falsy values\r\n            this[name] = value === 0 ? 0 : value ? value.valueOf() : \"\";\r\n        };\r\n    }\r\n    const cache$1 = {};\r\n    /**\r\n     * Compiling blocks is a multi-step process:\r\n     *\r\n     * 1. build an IntermediateTree from the HTML element. This intermediate tree\r\n     *    is a binary tree structure that encode dynamic info sub nodes, and the\r\n     *    path required to reach them\r\n     * 2. process the tree to build a block context, which is an object that aggregate\r\n     *    all dynamic info in a list, and also, all ref indexes.\r\n     * 3. process the context to build appropriate builder/setter functions\r\n     * 4. make a dynamic block class, which will efficiently collect references and\r\n     *    create/update dynamic locations/children\r\n     *\r\n     * @param str\r\n     * @returns a new block type, that can build concrete blocks\r\n     */\r\n    function createBlock(str) {\r\n        if (str in cache$1) {\r\n            return cache$1[str];\r\n        }\r\n        // step 0: prepare html base element\r\n        const doc = new DOMParser().parseFromString(`<t>${str}</t>`, \"text/xml\");\r\n        const node = doc.firstChild.firstChild;\r\n        if (config.shouldNormalizeDom) {\r\n            normalizeNode(node);\r\n        }\r\n        // step 1: prepare intermediate tree\r\n        const tree = buildTree(node);\r\n        // step 2: prepare block context\r\n        const context = buildContext(tree);\r\n        // step 3: build the final block class\r\n        const template = tree.el;\r\n        const Block = buildBlock(template, context);\r\n        cache$1[str] = Block;\r\n        return Block;\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Helper\r\n    // -----------------------------------------------------------------------------\r\n    function normalizeNode(node) {\r\n        if (node.nodeType === Node.TEXT_NODE) {\r\n            if (!/\\S/.test(node.textContent)) {\r\n                node.remove();\r\n                return;\r\n            }\r\n        }\r\n        if (node.nodeType === Node.ELEMENT_NODE) {\r\n            if (node.tagName === \"pre\") {\r\n                return;\r\n            }\r\n        }\r\n        for (let i = node.childNodes.length - 1; i >= 0; --i) {\r\n            normalizeNode(node.childNodes.item(i));\r\n        }\r\n    }\r\n    function buildTree(node, parent = null, domParentTree = null) {\r\n        switch (node.nodeType) {\r\n            case Node.ELEMENT_NODE: {\r\n                // HTMLElement\r\n                let currentNS = domParentTree && domParentTree.currentNS;\r\n                const tagName = node.tagName;\r\n                let el = undefined;\r\n                const info = [];\r\n                if (tagName.startsWith(\"block-text-\")) {\r\n                    const index = parseInt(tagName.slice(11), 10);\r\n                    info.push({ type: \"text\", idx: index });\r\n                    el = document.createTextNode(\"\");\r\n                }\r\n                if (tagName.startsWith(\"block-child-\")) {\r\n                    if (!domParentTree.isRef) {\r\n                        addRef(domParentTree);\r\n                    }\r\n                    const index = parseInt(tagName.slice(12), 10);\r\n                    info.push({ type: \"child\", idx: index });\r\n                    el = document.createTextNode(\"\");\r\n                }\r\n                currentNS || (currentNS = node.namespaceURI);\r\n                if (!el) {\r\n                    el = currentNS\r\n                        ? document.createElementNS(currentNS, tagName)\r\n                        : document.createElement(tagName);\r\n                }\r\n                if (el instanceof Element) {\r\n                    if (!domParentTree) {\r\n                        // some html elements may have side effects when setting their attributes.\r\n                        // For example, setting the src attribute of an <img/> will trigger a\r\n                        // request to get the corresponding image. This is something that we\r\n                        // don't want at compile time. We avoid that by putting the content of\r\n                        // the block in a <template/> element\r\n                        const fragment = document.createElement(\"template\").content;\r\n                        fragment.appendChild(el);\r\n                    }\r\n                    const attrs = node.attributes;\r\n                    for (let i = 0; i < attrs.length; i++) {\r\n                        const attrName = attrs[i].name;\r\n                        const attrValue = attrs[i].value;\r\n                        if (attrName.startsWith(\"block-handler-\")) {\r\n                            const idx = parseInt(attrName.slice(14), 10);\r\n                            info.push({\r\n                                type: \"handler\",\r\n                                idx,\r\n                                event: attrValue,\r\n                            });\r\n                        }\r\n                        else if (attrName.startsWith(\"block-attribute-\")) {\r\n                            const idx = parseInt(attrName.slice(16), 10);\r\n                            info.push({\r\n                                type: \"attribute\",\r\n                                idx,\r\n                                name: attrValue,\r\n                                tag: tagName,\r\n                            });\r\n                        }\r\n                        else if (attrName.startsWith(\"block-property-\")) {\r\n                            const idx = parseInt(attrName.slice(15), 10);\r\n                            info.push({\r\n                                type: \"property\",\r\n                                idx,\r\n                                name: attrValue,\r\n                                tag: tagName,\r\n                            });\r\n                        }\r\n                        else if (attrName === \"block-attributes\") {\r\n                            info.push({\r\n                                type: \"attributes\",\r\n                                idx: parseInt(attrValue, 10),\r\n                            });\r\n                        }\r\n                        else if (attrName === \"block-ref\") {\r\n                            info.push({\r\n                                type: \"ref\",\r\n                                idx: parseInt(attrValue, 10),\r\n                            });\r\n                        }\r\n                        else {\r\n                            el.setAttribute(attrs[i].name, attrValue);\r\n                        }\r\n                    }\r\n                }\r\n                const tree = {\r\n                    parent,\r\n                    firstChild: null,\r\n                    nextSibling: null,\r\n                    el,\r\n                    info,\r\n                    refN: 0,\r\n                    currentNS,\r\n                };\r\n                if (node.firstChild) {\r\n                    const childNode = node.childNodes[0];\r\n                    if (node.childNodes.length === 1 &&\r\n                        childNode.nodeType === Node.ELEMENT_NODE &&\r\n                        childNode.tagName.startsWith(\"block-child-\")) {\r\n                        const tagName = childNode.tagName;\r\n                        const index = parseInt(tagName.slice(12), 10);\r\n                        info.push({ idx: index, type: \"child\", isOnlyChild: true });\r\n                    }\r\n                    else {\r\n                        tree.firstChild = buildTree(node.firstChild, tree, tree);\r\n                        el.appendChild(tree.firstChild.el);\r\n                        let curNode = node.firstChild;\r\n                        let curTree = tree.firstChild;\r\n                        while ((curNode = curNode.nextSibling)) {\r\n                            curTree.nextSibling = buildTree(curNode, curTree, tree);\r\n                            el.appendChild(curTree.nextSibling.el);\r\n                            curTree = curTree.nextSibling;\r\n                        }\r\n                    }\r\n                }\r\n                if (tree.info.length) {\r\n                    addRef(tree);\r\n                }\r\n                return tree;\r\n            }\r\n            case Node.TEXT_NODE:\r\n            case Node.COMMENT_NODE: {\r\n                // text node or comment node\r\n                const el = node.nodeType === Node.TEXT_NODE\r\n                    ? document.createTextNode(node.textContent)\r\n                    : document.createComment(node.textContent);\r\n                return {\r\n                    parent: parent,\r\n                    firstChild: null,\r\n                    nextSibling: null,\r\n                    el,\r\n                    info: [],\r\n                    refN: 0,\r\n                    currentNS: null,\r\n                };\r\n            }\r\n        }\r\n        throw new OwlError(\"boom\");\r\n    }\r\n    function addRef(tree) {\r\n        tree.isRef = true;\r\n        do {\r\n            tree.refN++;\r\n        } while ((tree = tree.parent));\r\n    }\r\n    function parentTree(tree) {\r\n        let parent = tree.parent;\r\n        while (parent && parent.nextSibling === tree) {\r\n            tree = parent;\r\n            parent = parent.parent;\r\n        }\r\n        return parent;\r\n    }\r\n    function buildContext(tree, ctx, fromIdx) {\r\n        if (!ctx) {\r\n            const children = new Array(tree.info.filter((v) => v.type === \"child\").length);\r\n            ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN, refList: [] };\r\n            fromIdx = 0;\r\n        }\r\n        if (tree.refN) {\r\n            const initialIdx = fromIdx;\r\n            const isRef = tree.isRef;\r\n            const firstChild = tree.firstChild ? tree.firstChild.refN : 0;\r\n            const nextSibling = tree.nextSibling ? tree.nextSibling.refN : 0;\r\n            //node\r\n            if (isRef) {\r\n                for (let info of tree.info) {\r\n                    info.refIdx = initialIdx;\r\n                }\r\n                tree.refIdx = initialIdx;\r\n                updateCtx(ctx, tree);\r\n                fromIdx++;\r\n            }\r\n            // right\r\n            if (nextSibling) {\r\n                const idx = fromIdx + firstChild;\r\n                ctx.collectors.push({ idx, prevIdx: initialIdx, getVal: nodeGetNextSibling });\r\n                buildContext(tree.nextSibling, ctx, idx);\r\n            }\r\n            // left\r\n            if (firstChild) {\r\n                ctx.collectors.push({ idx: fromIdx, prevIdx: initialIdx, getVal: nodeGetFirstChild });\r\n                buildContext(tree.firstChild, ctx, fromIdx);\r\n            }\r\n        }\r\n        return ctx;\r\n    }\r\n    function updateCtx(ctx, tree) {\r\n        for (let info of tree.info) {\r\n            switch (info.type) {\r\n                case \"text\":\r\n                    ctx.locations.push({\r\n                        idx: info.idx,\r\n                        refIdx: info.refIdx,\r\n                        setData: setText,\r\n                        updateData: setText,\r\n                    });\r\n                    break;\r\n                case \"child\":\r\n                    if (info.isOnlyChild) {\r\n                        // tree is the parentnode here\r\n                        ctx.children[info.idx] = {\r\n                            parentRefIdx: info.refIdx,\r\n                            isOnlyChild: true,\r\n                        };\r\n                    }\r\n                    else {\r\n                        // tree is the anchor text node\r\n                        ctx.children[info.idx] = {\r\n                            parentRefIdx: parentTree(tree).refIdx,\r\n                            afterRefIdx: info.refIdx,\r\n                        };\r\n                    }\r\n                    break;\r\n                case \"property\": {\r\n                    const refIdx = info.refIdx;\r\n                    const setProp = makePropSetter(info.name);\r\n                    ctx.locations.push({\r\n                        idx: info.idx,\r\n                        refIdx,\r\n                        setData: setProp,\r\n                        updateData: setProp,\r\n                    });\r\n                    break;\r\n                }\r\n                case \"attribute\": {\r\n                    const refIdx = info.refIdx;\r\n                    let updater;\r\n                    let setter;\r\n                    if (info.name === \"class\") {\r\n                        setter = setClass;\r\n                        updater = updateClass;\r\n                    }\r\n                    else {\r\n                        setter = createAttrUpdater(info.name);\r\n                        updater = setter;\r\n                    }\r\n                    ctx.locations.push({\r\n                        idx: info.idx,\r\n                        refIdx,\r\n                        setData: setter,\r\n                        updateData: updater,\r\n                    });\r\n                    break;\r\n                }\r\n                case \"attributes\":\r\n                    ctx.locations.push({\r\n                        idx: info.idx,\r\n                        refIdx: info.refIdx,\r\n                        setData: attrsSetter,\r\n                        updateData: attrsUpdater,\r\n                    });\r\n                    break;\r\n                case \"handler\": {\r\n                    const { setup, update } = createEventHandler(info.event);\r\n                    ctx.locations.push({\r\n                        idx: info.idx,\r\n                        refIdx: info.refIdx,\r\n                        setData: setup,\r\n                        updateData: update,\r\n                    });\r\n                    break;\r\n                }\r\n                case \"ref\":\r\n                    const index = ctx.cbRefs.push(info.idx) - 1;\r\n                    ctx.locations.push({\r\n                        idx: info.idx,\r\n                        refIdx: info.refIdx,\r\n                        setData: makeRefSetter(index, ctx.refList),\r\n                        updateData: NO_OP,\r\n                    });\r\n            }\r\n        }\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // building the concrete block class\r\n    // -----------------------------------------------------------------------------\r\n    function buildBlock(template, ctx) {\r\n        let B = createBlockClass(template, ctx);\r\n        if (ctx.cbRefs.length) {\r\n            const cbRefs = ctx.cbRefs;\r\n            const refList = ctx.refList;\r\n            let cbRefsNumber = cbRefs.length;\r\n            B = class extends B {\r\n                mount(parent, afterNode) {\r\n                    refList.push(new Array(cbRefsNumber));\r\n                    super.mount(parent, afterNode);\r\n                    for (let cbRef of refList.pop()) {\r\n                        cbRef();\r\n                    }\r\n                }\r\n                remove() {\r\n                    super.remove();\r\n                    for (let cbRef of cbRefs) {\r\n                        let fn = this.data[cbRef];\r\n                        fn(null);\r\n                    }\r\n                }\r\n            };\r\n        }\r\n        if (ctx.children.length) {\r\n            B = class extends B {\r\n                constructor(data, children) {\r\n                    super(data);\r\n                    this.children = children;\r\n                }\r\n            };\r\n            B.prototype.beforeRemove = VMulti.prototype.beforeRemove;\r\n            return (data, children = []) => new B(data, children);\r\n        }\r\n        return (data) => new B(data);\r\n    }\r\n    function createBlockClass(template, ctx) {\r\n        const { refN, collectors, children } = ctx;\r\n        const colN = collectors.length;\r\n        ctx.locations.sort((a, b) => a.idx - b.idx);\r\n        const locations = ctx.locations.map((loc) => ({\r\n            refIdx: loc.refIdx,\r\n            setData: loc.setData,\r\n            updateData: loc.updateData,\r\n        }));\r\n        const locN = locations.length;\r\n        const childN = children.length;\r\n        const childrenLocs = children;\r\n        const isDynamic = refN > 0;\r\n        // these values are defined here to make them faster to lookup in the class\r\n        // block scope\r\n        const nodeCloneNode = nodeProto$2.cloneNode;\r\n        const nodeInsertBefore = nodeProto$2.insertBefore;\r\n        const elementRemove = elementProto.remove;\r\n        class Block {\r\n            constructor(data) {\r\n                this.data = data;\r\n            }\r\n            beforeRemove() { }\r\n            remove() {\r\n                elementRemove.call(this.el);\r\n            }\r\n            firstNode() {\r\n                return this.el;\r\n            }\r\n            moveBeforeDOMNode(node, parent = this.parentEl) {\r\n                this.parentEl = parent;\r\n                nodeInsertBefore.call(parent, this.el, node);\r\n            }\r\n            moveBeforeVNode(other, afterNode) {\r\n                nodeInsertBefore.call(this.parentEl, this.el, other ? other.el : afterNode);\r\n            }\r\n            toString() {\r\n                const div = document.createElement(\"div\");\r\n                this.mount(div, null);\r\n                return div.innerHTML;\r\n            }\r\n            mount(parent, afterNode) {\r\n                const el = nodeCloneNode.call(template, true);\r\n                nodeInsertBefore.call(parent, el, afterNode);\r\n                this.el = el;\r\n                this.parentEl = parent;\r\n            }\r\n            patch(other, withBeforeRemove) { }\r\n        }\r\n        if (isDynamic) {\r\n            Block.prototype.mount = function mount(parent, afterNode) {\r\n                const el = nodeCloneNode.call(template, true);\r\n                // collecting references\r\n                const refs = new Array(refN);\r\n                this.refs = refs;\r\n                refs[0] = el;\r\n                for (let i = 0; i < colN; i++) {\r\n                    const w = collectors[i];\r\n                    refs[w.idx] = w.getVal.call(refs[w.prevIdx]);\r\n                }\r\n                // applying data to all update points\r\n                if (locN) {\r\n                    const data = this.data;\r\n                    for (let i = 0; i < locN; i++) {\r\n                        const loc = locations[i];\r\n                        loc.setData.call(refs[loc.refIdx], data[i]);\r\n                    }\r\n                }\r\n                nodeInsertBefore.call(parent, el, afterNode);\r\n                // preparing all children\r\n                if (childN) {\r\n                    const children = this.children;\r\n                    for (let i = 0; i < childN; i++) {\r\n                        const child = children[i];\r\n                        if (child) {\r\n                            const loc = childrenLocs[i];\r\n                            const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;\r\n                            child.isOnlyChild = loc.isOnlyChild;\r\n                            child.mount(refs[loc.parentRefIdx], afterNode);\r\n                        }\r\n                    }\r\n                }\r\n                this.el = el;\r\n                this.parentEl = parent;\r\n            };\r\n            Block.prototype.patch = function patch(other, withBeforeRemove) {\r\n                if (this === other) {\r\n                    return;\r\n                }\r\n                const refs = this.refs;\r\n                // update texts/attributes/\r\n                if (locN) {\r\n                    const data1 = this.data;\r\n                    const data2 = other.data;\r\n                    for (let i = 0; i < locN; i++) {\r\n                        const val1 = data1[i];\r\n                        const val2 = data2[i];\r\n                        if (val1 !== val2) {\r\n                            const loc = locations[i];\r\n                            loc.updateData.call(refs[loc.refIdx], val2, val1);\r\n                        }\r\n                    }\r\n                    this.data = data2;\r\n                }\r\n                // update children\r\n                if (childN) {\r\n                    let children1 = this.children;\r\n                    const children2 = other.children;\r\n                    for (let i = 0; i < childN; i++) {\r\n                        const child1 = children1[i];\r\n                        const child2 = children2[i];\r\n                        if (child1) {\r\n                            if (child2) {\r\n                                child1.patch(child2, withBeforeRemove);\r\n                            }\r\n                            else {\r\n                                if (withBeforeRemove) {\r\n                                    child1.beforeRemove();\r\n                                }\r\n                                child1.remove();\r\n                                children1[i] = undefined;\r\n                            }\r\n                        }\r\n                        else if (child2) {\r\n                            const loc = childrenLocs[i];\r\n                            const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;\r\n                            child2.mount(refs[loc.parentRefIdx], afterNode);\r\n                            children1[i] = child2;\r\n                        }\r\n                    }\r\n                }\r\n            };\r\n        }\r\n        return Block;\r\n    }\r\n    function setText(value) {\r\n        characterDataSetData.call(this, toText(value));\r\n    }\r\n    function makeRefSetter(index, refs) {\r\n        return function setRef(fn) {\r\n            refs[refs.length - 1][index] = () => fn(this);\r\n        };\r\n    }\r\n\r\n    const getDescriptor = (o, p) => Object.getOwnPropertyDescriptor(o, p);\r\n    const nodeProto$1 = Node.prototype;\r\n    const nodeInsertBefore$1 = nodeProto$1.insertBefore;\r\n    const nodeAppendChild = nodeProto$1.appendChild;\r\n    const nodeRemoveChild$1 = nodeProto$1.removeChild;\r\n    const nodeSetTextContent = getDescriptor(nodeProto$1, \"textContent\").set;\r\n    // -----------------------------------------------------------------------------\r\n    // List Node\r\n    // -----------------------------------------------------------------------------\r\n    class VList {\r\n        constructor(children) {\r\n            this.children = children;\r\n        }\r\n        mount(parent, afterNode) {\r\n            const children = this.children;\r\n            const _anchor = document.createTextNode(\"\");\r\n            this.anchor = _anchor;\r\n            nodeInsertBefore$1.call(parent, _anchor, afterNode);\r\n            const l = children.length;\r\n            if (l) {\r\n                const mount = children[0].mount;\r\n                for (let i = 0; i < l; i++) {\r\n                    mount.call(children[i], parent, _anchor);\r\n                }\r\n            }\r\n            this.parentEl = parent;\r\n        }\r\n        moveBeforeDOMNode(node, parent = this.parentEl) {\r\n            this.parentEl = parent;\r\n            const children = this.children;\r\n            for (let i = 0, l = children.length; i < l; i++) {\r\n                children[i].moveBeforeDOMNode(node, parent);\r\n            }\r\n            parent.insertBefore(this.anchor, node);\r\n        }\r\n        moveBeforeVNode(other, afterNode) {\r\n            if (other) {\r\n                const next = other.children[0];\r\n                afterNode = (next ? next.firstNode() : other.anchor) || null;\r\n            }\r\n            const children = this.children;\r\n            for (let i = 0, l = children.length; i < l; i++) {\r\n                children[i].moveBeforeVNode(null, afterNode);\r\n            }\r\n            this.parentEl.insertBefore(this.anchor, afterNode);\r\n        }\r\n        patch(other, withBeforeRemove) {\r\n            if (this === other) {\r\n                return;\r\n            }\r\n            const ch1 = this.children;\r\n            const ch2 = other.children;\r\n            if (ch2.length === 0 && ch1.length === 0) {\r\n                return;\r\n            }\r\n            this.children = ch2;\r\n            const proto = ch2[0] || ch1[0];\r\n            const { mount: cMount, patch: cPatch, remove: cRemove, beforeRemove, moveBeforeVNode: cMoveBefore, firstNode: cFirstNode, } = proto;\r\n            const _anchor = this.anchor;\r\n            const isOnlyChild = this.isOnlyChild;\r\n            const parent = this.parentEl;\r\n            // fast path: no new child => only remove\r\n            if (ch2.length === 0 && isOnlyChild) {\r\n                if (withBeforeRemove) {\r\n                    for (let i = 0, l = ch1.length; i < l; i++) {\r\n                        beforeRemove.call(ch1[i]);\r\n                    }\r\n                }\r\n                nodeSetTextContent.call(parent, \"\");\r\n                nodeAppendChild.call(parent, _anchor);\r\n                return;\r\n            }\r\n            let startIdx1 = 0;\r\n            let startIdx2 = 0;\r\n            let startVn1 = ch1[0];\r\n            let startVn2 = ch2[0];\r\n            let endIdx1 = ch1.length - 1;\r\n            let endIdx2 = ch2.length - 1;\r\n            let endVn1 = ch1[endIdx1];\r\n            let endVn2 = ch2[endIdx2];\r\n            let mapping = undefined;\r\n            while (startIdx1 <= endIdx1 && startIdx2 <= endIdx2) {\r\n                // -------------------------------------------------------------------\r\n                if (startVn1 === null) {\r\n                    startVn1 = ch1[++startIdx1];\r\n                    continue;\r\n                }\r\n                // -------------------------------------------------------------------\r\n                if (endVn1 === null) {\r\n                    endVn1 = ch1[--endIdx1];\r\n                    continue;\r\n                }\r\n                // -------------------------------------------------------------------\r\n                let startKey1 = startVn1.key;\r\n                let startKey2 = startVn2.key;\r\n                if (startKey1 === startKey2) {\r\n                    cPatch.call(startVn1, startVn2, withBeforeRemove);\r\n                    ch2[startIdx2] = startVn1;\r\n                    startVn1 = ch1[++startIdx1];\r\n                    startVn2 = ch2[++startIdx2];\r\n                    continue;\r\n                }\r\n                // -------------------------------------------------------------------\r\n                let endKey1 = endVn1.key;\r\n                let endKey2 = endVn2.key;\r\n                if (endKey1 === endKey2) {\r\n                    cPatch.call(endVn1, endVn2, withBeforeRemove);\r\n                    ch2[endIdx2] = endVn1;\r\n                    endVn1 = ch1[--endIdx1];\r\n                    endVn2 = ch2[--endIdx2];\r\n                    continue;\r\n                }\r\n                // -------------------------------------------------------------------\r\n                if (startKey1 === endKey2) {\r\n                    // bnode moved right\r\n                    cPatch.call(startVn1, endVn2, withBeforeRemove);\r\n                    ch2[endIdx2] = startVn1;\r\n                    const nextChild = ch2[endIdx2 + 1];\r\n                    cMoveBefore.call(startVn1, nextChild, _anchor);\r\n                    startVn1 = ch1[++startIdx1];\r\n                    endVn2 = ch2[--endIdx2];\r\n                    continue;\r\n                }\r\n                // -------------------------------------------------------------------\r\n                if (endKey1 === startKey2) {\r\n                    // bnode moved left\r\n                    cPatch.call(endVn1, startVn2, withBeforeRemove);\r\n                    ch2[startIdx2] = endVn1;\r\n                    const nextChild = ch1[startIdx1];\r\n                    cMoveBefore.call(endVn1, nextChild, _anchor);\r\n                    endVn1 = ch1[--endIdx1];\r\n                    startVn2 = ch2[++startIdx2];\r\n                    continue;\r\n                }\r\n                // -------------------------------------------------------------------\r\n                mapping = mapping || createMapping(ch1, startIdx1, endIdx1);\r\n                let idxInOld = mapping[startKey2];\r\n                if (idxInOld === undefined) {\r\n                    cMount.call(startVn2, parent, cFirstNode.call(startVn1) || null);\r\n                }\r\n                else {\r\n                    const elmToMove = ch1[idxInOld];\r\n                    cMoveBefore.call(elmToMove, startVn1, null);\r\n                    cPatch.call(elmToMove, startVn2, withBeforeRemove);\r\n                    ch2[startIdx2] = elmToMove;\r\n                    ch1[idxInOld] = null;\r\n                }\r\n                startVn2 = ch2[++startIdx2];\r\n            }\r\n            // ---------------------------------------------------------------------\r\n            if (startIdx1 <= endIdx1 || startIdx2 <= endIdx2) {\r\n                if (startIdx1 > endIdx1) {\r\n                    const nextChild = ch2[endIdx2 + 1];\r\n                    const anchor = nextChild ? cFirstNode.call(nextChild) || null : _anchor;\r\n                    for (let i = startIdx2; i <= endIdx2; i++) {\r\n                        cMount.call(ch2[i], parent, anchor);\r\n                    }\r\n                }\r\n                else {\r\n                    for (let i = startIdx1; i <= endIdx1; i++) {\r\n                        let ch = ch1[i];\r\n                        if (ch) {\r\n                            if (withBeforeRemove) {\r\n                                beforeRemove.call(ch);\r\n                            }\r\n                            cRemove.call(ch);\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n        }\r\n        beforeRemove() {\r\n            const children = this.children;\r\n            const l = children.length;\r\n            if (l) {\r\n                const beforeRemove = children[0].beforeRemove;\r\n                for (let i = 0; i < l; i++) {\r\n                    beforeRemove.call(children[i]);\r\n                }\r\n            }\r\n        }\r\n        remove() {\r\n            const { parentEl, anchor } = this;\r\n            if (this.isOnlyChild) {\r\n                nodeSetTextContent.call(parentEl, \"\");\r\n            }\r\n            else {\r\n                const children = this.children;\r\n                const l = children.length;\r\n                if (l) {\r\n                    const remove = children[0].remove;\r\n                    for (let i = 0; i < l; i++) {\r\n                        remove.call(children[i]);\r\n                    }\r\n                }\r\n                nodeRemoveChild$1.call(parentEl, anchor);\r\n            }\r\n        }\r\n        firstNode() {\r\n            const child = this.children[0];\r\n            return child ? child.firstNode() : undefined;\r\n        }\r\n        toString() {\r\n            return this.children.map((c) => c.toString()).join(\"\");\r\n        }\r\n    }\r\n    function list(children) {\r\n        return new VList(children);\r\n    }\r\n    function createMapping(ch1, startIdx1, endIdx2) {\r\n        let mapping = {};\r\n        for (let i = startIdx1; i <= endIdx2; i++) {\r\n            mapping[ch1[i].key] = i;\r\n        }\r\n        return mapping;\r\n    }\r\n\r\n    const nodeProto = Node.prototype;\r\n    const nodeInsertBefore = nodeProto.insertBefore;\r\n    const nodeRemoveChild = nodeProto.removeChild;\r\n    class VHtml {\r\n        constructor(html) {\r\n            this.content = [];\r\n            this.html = html;\r\n        }\r\n        mount(parent, afterNode) {\r\n            this.parentEl = parent;\r\n            const template = document.createElement(\"template\");\r\n            template.innerHTML = this.html;\r\n            this.content = [...template.content.childNodes];\r\n            for (let elem of this.content) {\r\n                nodeInsertBefore.call(parent, elem, afterNode);\r\n            }\r\n            if (!this.content.length) {\r\n                const textNode = document.createTextNode(\"\");\r\n                this.content.push(textNode);\r\n                nodeInsertBefore.call(parent, textNode, afterNode);\r\n            }\r\n        }\r\n        moveBeforeDOMNode(node, parent = this.parentEl) {\r\n            this.parentEl = parent;\r\n            for (let elem of this.content) {\r\n                nodeInsertBefore.call(parent, elem, node);\r\n            }\r\n        }\r\n        moveBeforeVNode(other, afterNode) {\r\n            const target = other ? other.content[0] : afterNode;\r\n            this.moveBeforeDOMNode(target);\r\n        }\r\n        patch(other) {\r\n            if (this === other) {\r\n                return;\r\n            }\r\n            const html2 = other.html;\r\n            if (this.html !== html2) {\r\n                const parent = this.parentEl;\r\n                // insert new html in front of current\r\n                const afterNode = this.content[0];\r\n                const template = document.createElement(\"template\");\r\n                template.innerHTML = html2;\r\n                const content = [...template.content.childNodes];\r\n                for (let elem of content) {\r\n                    nodeInsertBefore.call(parent, elem, afterNode);\r\n                }\r\n                if (!content.length) {\r\n                    const textNode = document.createTextNode(\"\");\r\n                    content.push(textNode);\r\n                    nodeInsertBefore.call(parent, textNode, afterNode);\r\n                }\r\n                // remove current content\r\n                this.remove();\r\n                this.content = content;\r\n                this.html = other.html;\r\n            }\r\n        }\r\n        beforeRemove() { }\r\n        remove() {\r\n            const parent = this.parentEl;\r\n            for (let elem of this.content) {\r\n                nodeRemoveChild.call(parent, elem);\r\n            }\r\n        }\r\n        firstNode() {\r\n            return this.content[0];\r\n        }\r\n        toString() {\r\n            return this.html;\r\n        }\r\n    }\r\n    function html(str) {\r\n        return new VHtml(str);\r\n    }\r\n\r\n    function createCatcher(eventsSpec) {\r\n        const n = Object.keys(eventsSpec).length;\r\n        class VCatcher {\r\n            constructor(child, handlers) {\r\n                this.handlerFns = [];\r\n                this.afterNode = null;\r\n                this.child = child;\r\n                this.handlerData = handlers;\r\n            }\r\n            mount(parent, afterNode) {\r\n                this.parentEl = parent;\r\n                this.child.mount(parent, afterNode);\r\n                this.afterNode = document.createTextNode(\"\");\r\n                parent.insertBefore(this.afterNode, afterNode);\r\n                this.wrapHandlerData();\r\n                for (let name in eventsSpec) {\r\n                    const index = eventsSpec[name];\r\n                    const handler = createEventHandler(name);\r\n                    this.handlerFns[index] = handler;\r\n                    handler.setup.call(parent, this.handlerData[index]);\r\n                }\r\n            }\r\n            wrapHandlerData() {\r\n                for (let i = 0; i < n; i++) {\r\n                    let handler = this.handlerData[i];\r\n                    // handler = [...mods, fn, comp], so we need to replace second to last elem\r\n                    let idx = handler.length - 2;\r\n                    let origFn = handler[idx];\r\n                    const self = this;\r\n                    handler[idx] = function (ev) {\r\n                        const target = ev.target;\r\n                        let currentNode = self.child.firstNode();\r\n                        const afterNode = self.afterNode;\r\n                        while (currentNode && currentNode !== afterNode) {\r\n                            if (currentNode.contains(target)) {\r\n                                return origFn.call(this, ev);\r\n                            }\r\n                            currentNode = currentNode.nextSibling;\r\n                        }\r\n                    };\r\n                }\r\n            }\r\n            moveBeforeDOMNode(node, parent = this.parentEl) {\r\n                this.parentEl = parent;\r\n                this.child.moveBeforeDOMNode(node, parent);\r\n                parent.insertBefore(this.afterNode, node);\r\n            }\r\n            moveBeforeVNode(other, afterNode) {\r\n                if (other) {\r\n                    // check this with @ged-odoo for use in foreach\r\n                    afterNode = other.firstNode() || afterNode;\r\n                }\r\n                this.child.moveBeforeVNode(other ? other.child : null, afterNode);\r\n                this.parentEl.insertBefore(this.afterNode, afterNode);\r\n            }\r\n            patch(other, withBeforeRemove) {\r\n                if (this === other) {\r\n                    return;\r\n                }\r\n                this.handlerData = other.handlerData;\r\n                this.wrapHandlerData();\r\n                for (let i = 0; i < n; i++) {\r\n                    this.handlerFns[i].update.call(this.parentEl, this.handlerData[i]);\r\n                }\r\n                this.child.patch(other.child, withBeforeRemove);\r\n            }\r\n            beforeRemove() {\r\n                this.child.beforeRemove();\r\n            }\r\n            remove() {\r\n                for (let i = 0; i < n; i++) {\r\n                    this.handlerFns[i].remove.call(this.parentEl);\r\n                }\r\n                this.child.remove();\r\n                this.afterNode.remove();\r\n            }\r\n            firstNode() {\r\n                return this.child.firstNode();\r\n            }\r\n            toString() {\r\n                return this.child.toString();\r\n            }\r\n        }\r\n        return function (child, handlers) {\r\n            return new VCatcher(child, handlers);\r\n        };\r\n    }\r\n\r\n    function mount$1(vnode, fixture, afterNode = null) {\r\n        vnode.mount(fixture, afterNode);\r\n    }\r\n    function patch(vnode1, vnode2, withBeforeRemove = false) {\r\n        vnode1.patch(vnode2, withBeforeRemove);\r\n    }\r\n    function remove(vnode, withBeforeRemove = false) {\r\n        if (withBeforeRemove) {\r\n            vnode.beforeRemove();\r\n        }\r\n        vnode.remove();\r\n    }\r\n\r\n    // Maps fibers to thrown errors\r\n    const fibersInError = new WeakMap();\r\n    const nodeErrorHandlers = new WeakMap();\r\n    function _handleError(node, error) {\r\n        if (!node) {\r\n            return false;\r\n        }\r\n        const fiber = node.fiber;\r\n        if (fiber) {\r\n            fibersInError.set(fiber, error);\r\n        }\r\n        const errorHandlers = nodeErrorHandlers.get(node);\r\n        if (errorHandlers) {\r\n            let handled = false;\r\n            // execute in the opposite order\r\n            for (let i = errorHandlers.length - 1; i >= 0; i--) {\r\n                try {\r\n                    errorHandlers[i](error);\r\n                    handled = true;\r\n                    break;\r\n                }\r\n                catch (e) {\r\n                    error = e;\r\n                }\r\n            }\r\n            if (handled) {\r\n                return true;\r\n            }\r\n        }\r\n        return _handleError(node.parent, error);\r\n    }\r\n    function handleError(params) {\r\n        let { error } = params;\r\n        // Wrap error if it wasn't wrapped by wrapError (ie when not in dev mode)\r\n        if (!(error instanceof OwlError)) {\r\n            error = Object.assign(new OwlError(`An error occured in the owl lifecycle (see this Error's \"cause\" property)`), { cause: error });\r\n        }\r\n        const node = \"node\" in params ? params.node : params.fiber.node;\r\n        const fiber = \"fiber\" in params ? params.fiber : node.fiber;\r\n        if (fiber) {\r\n            // resets the fibers on components if possible. This is important so that\r\n            // new renderings can be properly included in the initial one, if any.\r\n            let current = fiber;\r\n            do {\r\n                current.node.fiber = current;\r\n                current = current.parent;\r\n            } while (current);\r\n            fibersInError.set(fiber.root, error);\r\n        }\r\n        const handled = _handleError(node, error);\r\n        if (!handled) {\r\n            console.warn(`[Owl] Unhandled error. Destroying the root component`);\r\n            try {\r\n                node.app.destroy();\r\n            }\r\n            catch (e) {\r\n                console.error(e);\r\n            }\r\n            throw error;\r\n        }\r\n    }\r\n\r\n    function makeChildFiber(node, parent) {\r\n        let current = node.fiber;\r\n        if (current) {\r\n            cancelFibers(current.children);\r\n            current.root = null;\r\n        }\r\n        return new Fiber(node, parent);\r\n    }\r\n    function makeRootFiber(node) {\r\n        let current = node.fiber;\r\n        if (current) {\r\n            let root = current.root;\r\n            // lock root fiber because canceling children fibers may destroy components,\r\n            // which means any arbitrary code can be run in onWillDestroy, which may\r\n            // trigger new renderings\r\n            root.locked = true;\r\n            root.setCounter(root.counter + 1 - cancelFibers(current.children));\r\n            root.locked = false;\r\n            current.children = [];\r\n            current.childrenMap = {};\r\n            current.bdom = null;\r\n            if (fibersInError.has(current)) {\r\n                fibersInError.delete(current);\r\n                fibersInError.delete(root);\r\n                current.appliedToDom = false;\r\n            }\r\n            return current;\r\n        }\r\n        const fiber = new RootFiber(node, null);\r\n        if (node.willPatch.length) {\r\n            fiber.willPatch.push(fiber);\r\n        }\r\n        if (node.patched.length) {\r\n            fiber.patched.push(fiber);\r\n        }\r\n        return fiber;\r\n    }\r\n    function throwOnRender() {\r\n        throw new OwlError(\"Attempted to render cancelled fiber\");\r\n    }\r\n    /**\r\n     * @returns number of not-yet rendered fibers cancelled\r\n     */\r\n    function cancelFibers(fibers) {\r\n        let result = 0;\r\n        for (let fiber of fibers) {\r\n            let node = fiber.node;\r\n            fiber.render = throwOnRender;\r\n            if (node.status === 0 /* NEW */) {\r\n                node.cancel();\r\n            }\r\n            node.fiber = null;\r\n            if (fiber.bdom) {\r\n                // if fiber has been rendered, this means that the component props have\r\n                // been updated. however, this fiber will not be patched to the dom, so\r\n                // it could happen that the next render compare the current props with\r\n                // the same props, and skip the render completely. With the next line,\r\n                // we kindly request the component code to force a render, so it works as\r\n                // expected.\r\n                node.forceNextRender = true;\r\n            }\r\n            else {\r\n                result++;\r\n            }\r\n            result += cancelFibers(fiber.children);\r\n        }\r\n        return result;\r\n    }\r\n    class Fiber {\r\n        constructor(node, parent) {\r\n            this.bdom = null;\r\n            this.children = [];\r\n            this.appliedToDom = false;\r\n            this.deep = false;\r\n            this.childrenMap = {};\r\n            this.node = node;\r\n            this.parent = parent;\r\n            if (parent) {\r\n                this.deep = parent.deep;\r\n                const root = parent.root;\r\n                root.setCounter(root.counter + 1);\r\n                this.root = root;\r\n                parent.children.push(this);\r\n            }\r\n            else {\r\n                this.root = this;\r\n            }\r\n        }\r\n        render() {\r\n            // if some parent has a fiber => register in followup\r\n            let prev = this.root.node;\r\n            let scheduler = prev.app.scheduler;\r\n            let current = prev.parent;\r\n            while (current) {\r\n                if (current.fiber) {\r\n                    let root = current.fiber.root;\r\n                    if (root.counter === 0 && prev.parentKey in current.fiber.childrenMap) {\r\n                        current = root.node;\r\n                    }\r\n                    else {\r\n                        scheduler.delayedRenders.push(this);\r\n                        return;\r\n                    }\r\n                }\r\n                prev = current;\r\n                current = current.parent;\r\n            }\r\n            // there are no current rendering from above => we can render\r\n            this._render();\r\n        }\r\n        _render() {\r\n            const node = this.node;\r\n            const root = this.root;\r\n            if (root) {\r\n                try {\r\n                    this.bdom = true;\r\n                    this.bdom = node.renderFn();\r\n                }\r\n                catch (e) {\r\n                    node.app.handleError({ node, error: e });\r\n                }\r\n                root.setCounter(root.counter - 1);\r\n            }\r\n        }\r\n    }\r\n    class RootFiber extends Fiber {\r\n        constructor() {\r\n            super(...arguments);\r\n            this.counter = 1;\r\n            // only add stuff in this if they have registered some hooks\r\n            this.willPatch = [];\r\n            this.patched = [];\r\n            this.mounted = [];\r\n            // A fiber is typically locked when it is completing and the patch has not, or is being applied.\r\n            // i.e.: render triggered in onWillUnmount or in willPatch will be delayed\r\n            this.locked = false;\r\n        }\r\n        complete() {\r\n            const node = this.node;\r\n            this.locked = true;\r\n            let current = undefined;\r\n            try {\r\n                // Step 1: calling all willPatch lifecycle hooks\r\n                for (current of this.willPatch) {\r\n                    // because of the asynchronous nature of the rendering, some parts of the\r\n                    // UI may have been rendered, then deleted in a followup rendering, and we\r\n                    // do not want to call onWillPatch in that case.\r\n                    let node = current.node;\r\n                    if (node.fiber === current) {\r\n                        const component = node.component;\r\n                        for (let cb of node.willPatch) {\r\n                            cb.call(component);\r\n                        }\r\n                    }\r\n                }\r\n                current = undefined;\r\n                // Step 2: patching the dom\r\n                node._patch();\r\n                this.locked = false;\r\n                // Step 4: calling all mounted lifecycle hooks\r\n                let mountedFibers = this.mounted;\r\n                while ((current = mountedFibers.pop())) {\r\n                    current = current;\r\n                    if (current.appliedToDom) {\r\n                        for (let cb of current.node.mounted) {\r\n                            cb();\r\n                        }\r\n                    }\r\n                }\r\n                // Step 5: calling all patched hooks\r\n                let patchedFibers = this.patched;\r\n                while ((current = patchedFibers.pop())) {\r\n                    current = current;\r\n                    if (current.appliedToDom) {\r\n                        for (let cb of current.node.patched) {\r\n                            cb();\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n            catch (e) {\r\n                this.locked = false;\r\n                node.app.handleError({ fiber: current || this, error: e });\r\n            }\r\n        }\r\n        setCounter(newValue) {\r\n            this.counter = newValue;\r\n            if (newValue === 0) {\r\n                this.node.app.scheduler.flush();\r\n            }\r\n        }\r\n    }\r\n    class MountFiber extends RootFiber {\r\n        constructor(node, target, options = {}) {\r\n            super(node, null);\r\n            this.target = target;\r\n            this.position = options.position || \"last-child\";\r\n        }\r\n        complete() {\r\n            let current = this;\r\n            try {\r\n                const node = this.node;\r\n                node.children = this.childrenMap;\r\n                node.app.constructor.validateTarget(this.target);\r\n                if (node.bdom) {\r\n                    // this is a complicated situation: if we mount a fiber with an existing\r\n                    // bdom, this means that this same fiber was already completed, mounted,\r\n                    // but a crash occurred in some mounted hook. Then, it was handled and\r\n                    // the new rendering is being applied.\r\n                    node.updateDom();\r\n                }\r\n                else {\r\n                    node.bdom = this.bdom;\r\n                    if (this.position === \"last-child\" || this.target.childNodes.length === 0) {\r\n                        mount$1(node.bdom, this.target);\r\n                    }\r\n                    else {\r\n                        const firstChild = this.target.childNodes[0];\r\n                        mount$1(node.bdom, this.target, firstChild);\r\n                    }\r\n                }\r\n                // unregistering the fiber before mounted since it can do another render\r\n                // and that the current rendering is obviously completed\r\n                node.fiber = null;\r\n                node.status = 1 /* MOUNTED */;\r\n                this.appliedToDom = true;\r\n                let mountedFibers = this.mounted;\r\n                while ((current = mountedFibers.pop())) {\r\n                    if (current.appliedToDom) {\r\n                        for (let cb of current.node.mounted) {\r\n                            cb();\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n            catch (e) {\r\n                this.node.app.handleError({ fiber: current, error: e });\r\n            }\r\n        }\r\n    }\r\n\r\n    // Special key to subscribe to, to be notified of key creation/deletion\r\n    const KEYCHANGES = Symbol(\"Key changes\");\r\n    // Used to specify the absence of a callback, can be used as WeakMap key but\r\n    // should only be used as a sentinel value and never called.\r\n    const NO_CALLBACK = () => {\r\n        throw new Error(\"Called NO_CALLBACK. Owl is broken, please report this to the maintainers.\");\r\n    };\r\n    const objectToString = Object.prototype.toString;\r\n    const objectHasOwnProperty = Object.prototype.hasOwnProperty;\r\n    // Use arrays because Array.includes is faster than Set.has for small arrays\r\n    const SUPPORTED_RAW_TYPES = [\"Object\", \"Array\", \"Set\", \"Map\", \"WeakMap\"];\r\n    const COLLECTION_RAW_TYPES = [\"Set\", \"Map\", \"WeakMap\"];\r\n    /**\r\n     * extract \"RawType\" from strings like \"[object RawType]\" => this lets us ignore\r\n     * many native objects such as Promise (whose toString is [object Promise])\r\n     * or Date ([object Date]), while also supporting collections without using\r\n     * instanceof in a loop\r\n     *\r\n     * @param obj the object to check\r\n     * @returns the raw type of the object\r\n     */\r\n    function rawType(obj) {\r\n        return objectToString.call(toRaw(obj)).slice(8, -1);\r\n    }\r\n    /**\r\n     * Checks whether a given value can be made into a reactive object.\r\n     *\r\n     * @param value the value to check\r\n     * @returns whether the value can be made reactive\r\n     */\r\n    function canBeMadeReactive(value) {\r\n        if (typeof value !== \"object\") {\r\n            return false;\r\n        }\r\n        return SUPPORTED_RAW_TYPES.includes(rawType(value));\r\n    }\r\n    /**\r\n     * Creates a reactive from the given object/callback if possible and returns it,\r\n     * returns the original object otherwise.\r\n     *\r\n     * @param value the value make reactive\r\n     * @returns a reactive for the given object when possible, the original otherwise\r\n     */\r\n    function possiblyReactive(val, cb) {\r\n        return canBeMadeReactive(val) ? reactive(val, cb) : val;\r\n    }\r\n    const skipped = new WeakSet();\r\n    /**\r\n     * Mark an object or array so that it is ignored by the reactivity system\r\n     *\r\n     * @param value the value to mark\r\n     * @returns the object itself\r\n     */\r\n    function markRaw(value) {\r\n        skipped.add(value);\r\n        return value;\r\n    }\r\n    /**\r\n     * Given a reactive objet, return the raw (non reactive) underlying object\r\n     *\r\n     * @param value a reactive value\r\n     * @returns the underlying value\r\n     */\r\n    function toRaw(value) {\r\n        return targets.has(value) ? targets.get(value) : value;\r\n    }\r\n    const targetToKeysToCallbacks = new WeakMap();\r\n    /**\r\n     * Observes a given key on a target with an callback. The callback will be\r\n     * called when the given key changes on the target.\r\n     *\r\n     * @param target the target whose key should be observed\r\n     * @param key the key to observe (or Symbol(KEYCHANGES) for key creation\r\n     *  or deletion)\r\n     * @param callback the function to call when the key changes\r\n     */\r\n    function observeTargetKey(target, key, callback) {\r\n        if (callback === NO_CALLBACK) {\r\n            return;\r\n        }\r\n        if (!targetToKeysToCallbacks.get(target)) {\r\n            targetToKeysToCallbacks.set(target, new Map());\r\n        }\r\n        const keyToCallbacks = targetToKeysToCallbacks.get(target);\r\n        if (!keyToCallbacks.get(key)) {\r\n            keyToCallbacks.set(key, new Set());\r\n        }\r\n        keyToCallbacks.get(key).add(callback);\r\n        if (!callbacksToTargets.has(callback)) {\r\n            callbacksToTargets.set(callback, new Set());\r\n        }\r\n        callbacksToTargets.get(callback).add(target);\r\n    }\r\n    /**\r\n     * Notify Reactives that are observing a given target that a key has changed on\r\n     * the target.\r\n     *\r\n     * @param target target whose Reactives should be notified that the target was\r\n     *  changed.\r\n     * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created\r\n     *   or deleted)\r\n     */\r\n    function notifyReactives(target, key) {\r\n        const keyToCallbacks = targetToKeysToCallbacks.get(target);\r\n        if (!keyToCallbacks) {\r\n            return;\r\n        }\r\n        const callbacks = keyToCallbacks.get(key);\r\n        if (!callbacks) {\r\n            return;\r\n        }\r\n        // Loop on copy because clearReactivesForCallback will modify the set in place\r\n        for (const callback of [...callbacks]) {\r\n            clearReactivesForCallback(callback);\r\n            callback();\r\n        }\r\n    }\r\n    const callbacksToTargets = new WeakMap();\r\n    /**\r\n     * Clears all subscriptions of the Reactives associated with a given callback.\r\n     *\r\n     * @param callback the callback for which the reactives need to be cleared\r\n     */\r\n    function clearReactivesForCallback(callback) {\r\n        const targetsToClear = callbacksToTargets.get(callback);\r\n        if (!targetsToClear) {\r\n            return;\r\n        }\r\n        for (const target of targetsToClear) {\r\n            const observedKeys = targetToKeysToCallbacks.get(target);\r\n            if (!observedKeys) {\r\n                continue;\r\n            }\r\n            for (const [key, callbacks] of observedKeys.entries()) {\r\n                callbacks.delete(callback);\r\n                if (!callbacks.size) {\r\n                    observedKeys.delete(key);\r\n                }\r\n            }\r\n        }\r\n        targetsToClear.clear();\r\n    }\r\n    function getSubscriptions(callback) {\r\n        const targets = callbacksToTargets.get(callback) || [];\r\n        return [...targets].map((target) => {\r\n            const keysToCallbacks = targetToKeysToCallbacks.get(target);\r\n            let keys = [];\r\n            if (keysToCallbacks) {\r\n                for (const [key, cbs] of keysToCallbacks) {\r\n                    if (cbs.has(callback)) {\r\n                        keys.push(key);\r\n                    }\r\n                }\r\n            }\r\n            return { target, keys };\r\n        });\r\n    }\r\n    // Maps reactive objects to the underlying target\r\n    const targets = new WeakMap();\r\n    const reactiveCache = new WeakMap();\r\n    /**\r\n     * Creates a reactive proxy for an object. Reading data on the reactive object\r\n     * subscribes to changes to the data. Writing data on the object will cause the\r\n     * notify callback to be called if there are suscriptions to that data. Nested\r\n     * objects and arrays are automatically made reactive as well.\r\n     *\r\n     * Whenever you are notified of a change, all subscriptions are cleared, and if\r\n     * you would like to be notified of any further changes, you should go read\r\n     * the underlying data again. We assume that if you don't go read it again after\r\n     * being notified, it means that you are no longer interested in that data.\r\n     *\r\n     * Subscriptions:\r\n     * + Reading a property on an object will subscribe you to changes in the value\r\n     *    of that property.\r\n     * + Accessing an object's keys (eg with Object.keys or with `for..in`) will\r\n     *    subscribe you to the creation/deletion of keys. Checking the presence of a\r\n     *    key on the object with 'in' has the same effect.\r\n     * - getOwnPropertyDescriptor does not currently subscribe you to the property.\r\n     *    This is a choice that was made because changing a key's value will trigger\r\n     *    this trap and we do not want to subscribe by writes. This also means that\r\n     *    Object.hasOwnProperty doesn't subscribe as it goes through this trap.\r\n     *\r\n     * @param target the object for which to create a reactive proxy\r\n     * @param callback the function to call when an observed property of the\r\n     *  reactive has changed\r\n     * @returns a proxy that tracks changes to it\r\n     */\r\n    function reactive(target, callback = NO_CALLBACK) {\r\n        if (!canBeMadeReactive(target)) {\r\n            throw new OwlError(`Cannot make the given value reactive`);\r\n        }\r\n        if (skipped.has(target)) {\r\n            return target;\r\n        }\r\n        if (targets.has(target)) {\r\n            // target is reactive, create a reactive on the underlying object instead\r\n            return reactive(targets.get(target), callback);\r\n        }\r\n        if (!reactiveCache.has(target)) {\r\n            reactiveCache.set(target, new WeakMap());\r\n        }\r\n        const reactivesForTarget = reactiveCache.get(target);\r\n        if (!reactivesForTarget.has(callback)) {\r\n            const targetRawType = rawType(target);\r\n            const handler = COLLECTION_RAW_TYPES.includes(targetRawType)\r\n                ? collectionsProxyHandler(target, callback, targetRawType)\r\n                : basicProxyHandler(callback);\r\n            const proxy = new Proxy(target, handler);\r\n            reactivesForTarget.set(callback, proxy);\r\n            targets.set(proxy, target);\r\n        }\r\n        return reactivesForTarget.get(callback);\r\n    }\r\n    /**\r\n     * Creates a basic proxy handler for regular objects and arrays.\r\n     *\r\n     * @param callback @see reactive\r\n     * @returns a proxy handler object\r\n     */\r\n    function basicProxyHandler(callback) {\r\n        return {\r\n            get(target, key, receiver) {\r\n                // non-writable non-configurable properties cannot be made reactive\r\n                const desc = Object.getOwnPropertyDescriptor(target, key);\r\n                if (desc && !desc.writable && !desc.configurable) {\r\n                    return Reflect.get(target, key, receiver);\r\n                }\r\n                observeTargetKey(target, key, callback);\r\n                return possiblyReactive(Reflect.get(target, key, receiver), callback);\r\n            },\r\n            set(target, key, value, receiver) {\r\n                const hadKey = objectHasOwnProperty.call(target, key);\r\n                const originalValue = Reflect.get(target, key, receiver);\r\n                const ret = Reflect.set(target, key, toRaw(value), receiver);\r\n                if (!hadKey && objectHasOwnProperty.call(target, key)) {\r\n                    notifyReactives(target, KEYCHANGES);\r\n                }\r\n                // While Array length may trigger the set trap, it's not actually set by this\r\n                // method but is updated behind the scenes, and the trap is not called with the\r\n                // new value. We disable the \"same-value-optimization\" for it because of that.\r\n                if (originalValue !== Reflect.get(target, key, receiver) ||\r\n                    (key === \"length\" && Array.isArray(target))) {\r\n                    notifyReactives(target, key);\r\n                }\r\n                return ret;\r\n            },\r\n            deleteProperty(target, key) {\r\n                const ret = Reflect.deleteProperty(target, key);\r\n                // TODO: only notify when something was actually deleted\r\n                notifyReactives(target, KEYCHANGES);\r\n                notifyReactives(target, key);\r\n                return ret;\r\n            },\r\n            ownKeys(target) {\r\n                observeTargetKey(target, KEYCHANGES, callback);\r\n                return Reflect.ownKeys(target);\r\n            },\r\n            has(target, key) {\r\n                // TODO: this observes all key changes instead of only the presence of the argument key\r\n                // observing the key itself would observe value changes instead of presence changes\r\n                // so we may need a finer grained system to distinguish observing value vs presence.\r\n                observeTargetKey(target, KEYCHANGES, callback);\r\n                return Reflect.has(target, key);\r\n            },\r\n        };\r\n    }\r\n    /**\r\n     * Creates a function that will observe the key that is passed to it when called\r\n     * and delegates to the underlying method.\r\n     *\r\n     * @param methodName name of the method to delegate to\r\n     * @param target @see reactive\r\n     * @param callback @see reactive\r\n     */\r\n    function makeKeyObserver(methodName, target, callback) {\r\n        return (key) => {\r\n            key = toRaw(key);\r\n            observeTargetKey(target, key, callback);\r\n            return possiblyReactive(target[methodName](key), callback);\r\n        };\r\n    }\r\n    /**\r\n     * Creates an iterable that will delegate to the underlying iteration method and\r\n     * observe keys as necessary.\r\n     *\r\n     * @param methodName name of the method to delegate to\r\n     * @param target @see reactive\r\n     * @param callback @see reactive\r\n     */\r\n    function makeIteratorObserver(methodName, target, callback) {\r\n        return function* () {\r\n            observeTargetKey(target, KEYCHANGES, callback);\r\n            const keys = target.keys();\r\n            for (const item of target[methodName]()) {\r\n                const key = keys.next().value;\r\n                observeTargetKey(target, key, callback);\r\n                yield possiblyReactive(item, callback);\r\n            }\r\n        };\r\n    }\r\n    /**\r\n     * Creates a forEach function that will delegate to forEach on the underlying\r\n     * collection while observing key changes, and keys as they're iterated over,\r\n     * and making the passed keys/values reactive.\r\n     *\r\n     * @param target @see reactive\r\n     * @param callback @see reactive\r\n     */\r\n    function makeForEachObserver(target, callback) {\r\n        return function forEach(forEachCb, thisArg) {\r\n            observeTargetKey(target, KEYCHANGES, callback);\r\n            target.forEach(function (val, key, targetObj) {\r\n                observeTargetKey(target, key, callback);\r\n                forEachCb.call(thisArg, possiblyReactive(val, callback), possiblyReactive(key, callback), possiblyReactive(targetObj, callback));\r\n            }, thisArg);\r\n        };\r\n    }\r\n    /**\r\n     * Creates a function that will delegate to an underlying method, and check if\r\n     * that method has modified the presence or value of a key, and notify the\r\n     * reactives appropriately.\r\n     *\r\n     * @param setterName name of the method to delegate to\r\n     * @param getterName name of the method which should be used to retrieve the\r\n     *  value before calling the delegate method for comparison purposes\r\n     * @param target @see reactive\r\n     */\r\n    function delegateAndNotify(setterName, getterName, target) {\r\n        return (key, value) => {\r\n            key = toRaw(key);\r\n            const hadKey = target.has(key);\r\n            const originalValue = target[getterName](key);\r\n            const ret = target[setterName](key, value);\r\n            const hasKey = target.has(key);\r\n            if (hadKey !== hasKey) {\r\n                notifyReactives(target, KEYCHANGES);\r\n            }\r\n            if (originalValue !== target[getterName](key)) {\r\n                notifyReactives(target, key);\r\n            }\r\n            return ret;\r\n        };\r\n    }\r\n    /**\r\n     * Creates a function that will clear the underlying collection and notify that\r\n     * the keys of the collection have changed.\r\n     *\r\n     * @param target @see reactive\r\n     */\r\n    function makeClearNotifier(target) {\r\n        return () => {\r\n            const allKeys = [...target.keys()];\r\n            target.clear();\r\n            notifyReactives(target, KEYCHANGES);\r\n            for (const key of allKeys) {\r\n                notifyReactives(target, key);\r\n            }\r\n        };\r\n    }\r\n    /**\r\n     * Maps raw type of an object to an object containing functions that can be used\r\n     * to build an appropritate proxy handler for that raw type. Eg: when making a\r\n     * reactive set, calling the has method should mark the key that is being\r\n     * retrieved as observed, and calling the add or delete method should notify the\r\n     * reactives that the key which is being added or deleted has been modified.\r\n     */\r\n    const rawTypeToFuncHandlers = {\r\n        Set: (target, callback) => ({\r\n            has: makeKeyObserver(\"has\", target, callback),\r\n            add: delegateAndNotify(\"add\", \"has\", target),\r\n            delete: delegateAndNotify(\"delete\", \"has\", target),\r\n            keys: makeIteratorObserver(\"keys\", target, callback),\r\n            values: makeIteratorObserver(\"values\", target, callback),\r\n            entries: makeIteratorObserver(\"entries\", target, callback),\r\n            [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),\r\n            forEach: makeForEachObserver(target, callback),\r\n            clear: makeClearNotifier(target),\r\n            get size() {\r\n                observeTargetKey(target, KEYCHANGES, callback);\r\n                return target.size;\r\n            },\r\n        }),\r\n        Map: (target, callback) => ({\r\n            has: makeKeyObserver(\"has\", target, callback),\r\n            get: makeKeyObserver(\"get\", target, callback),\r\n            set: delegateAndNotify(\"set\", \"get\", target),\r\n            delete: delegateAndNotify(\"delete\", \"has\", target),\r\n            keys: makeIteratorObserver(\"keys\", target, callback),\r\n            values: makeIteratorObserver(\"values\", target, callback),\r\n            entries: makeIteratorObserver(\"entries\", target, callback),\r\n            [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),\r\n            forEach: makeForEachObserver(target, callback),\r\n            clear: makeClearNotifier(target),\r\n            get size() {\r\n                observeTargetKey(target, KEYCHANGES, callback);\r\n                return target.size;\r\n            },\r\n        }),\r\n        WeakMap: (target, callback) => ({\r\n            has: makeKeyObserver(\"has\", target, callback),\r\n            get: makeKeyObserver(\"get\", target, callback),\r\n            set: delegateAndNotify(\"set\", \"get\", target),\r\n            delete: delegateAndNotify(\"delete\", \"has\", target),\r\n        }),\r\n    };\r\n    /**\r\n     * Creates a proxy handler for collections (Set/Map/WeakMap)\r\n     *\r\n     * @param callback @see reactive\r\n     * @param target @see reactive\r\n     * @returns a proxy handler object\r\n     */\r\n    function collectionsProxyHandler(target, callback, targetRawType) {\r\n        // TODO: if performance is an issue we can create the special handlers lazily when each\r\n        // property is read.\r\n        const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);\r\n        return Object.assign(basicProxyHandler(callback), {\r\n            // FIXME: probably broken when part of prototype chain since we ignore the receiver\r\n            get(target, key) {\r\n                if (objectHasOwnProperty.call(specialHandlers, key)) {\r\n                    return specialHandlers[key];\r\n                }\r\n                observeTargetKey(target, key, callback);\r\n                return possiblyReactive(target[key], callback);\r\n            },\r\n        });\r\n    }\r\n\r\n    let currentNode = null;\r\n    function getCurrent() {\r\n        if (!currentNode) {\r\n            throw new OwlError(\"No active component (a hook function should only be called in 'setup')\");\r\n        }\r\n        return currentNode;\r\n    }\r\n    function useComponent() {\r\n        return currentNode.component;\r\n    }\r\n    /**\r\n     * Apply default props (only top level).\r\n     */\r\n    function applyDefaultProps(props, defaultProps) {\r\n        for (let propName in defaultProps) {\r\n            if (props[propName] === undefined) {\r\n                props[propName] = defaultProps[propName];\r\n            }\r\n        }\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Integration with reactivity system (useState)\r\n    // -----------------------------------------------------------------------------\r\n    const batchedRenderFunctions = new WeakMap();\r\n    /**\r\n     * Creates a reactive object that will be observed by the current component.\r\n     * Reading data from the returned object (eg during rendering) will cause the\r\n     * component to subscribe to that data and be rerendered when it changes.\r\n     *\r\n     * @param state the state to observe\r\n     * @returns a reactive object that will cause the component to re-render on\r\n     *  relevant changes\r\n     * @see reactive\r\n     */\r\n    function useState(state) {\r\n        const node = getCurrent();\r\n        let render = batchedRenderFunctions.get(node);\r\n        if (!render) {\r\n            render = batched(node.render.bind(node, false));\r\n            batchedRenderFunctions.set(node, render);\r\n            // manual implementation of onWillDestroy to break cyclic dependency\r\n            node.willDestroy.push(clearReactivesForCallback.bind(null, render));\r\n        }\r\n        return reactive(state, render);\r\n    }\r\n    class ComponentNode {\r\n        constructor(C, props, app, parent, parentKey) {\r\n            this.fiber = null;\r\n            this.bdom = null;\r\n            this.status = 0 /* NEW */;\r\n            this.forceNextRender = false;\r\n            this.nextProps = null;\r\n            this.children = Object.create(null);\r\n            this.refs = {};\r\n            this.willStart = [];\r\n            this.willUpdateProps = [];\r\n            this.willUnmount = [];\r\n            this.mounted = [];\r\n            this.willPatch = [];\r\n            this.patched = [];\r\n            this.willDestroy = [];\r\n            currentNode = this;\r\n            this.app = app;\r\n            this.parent = parent;\r\n            this.props = props;\r\n            this.parentKey = parentKey;\r\n            const defaultProps = C.defaultProps;\r\n            props = Object.assign({}, props);\r\n            if (defaultProps) {\r\n                applyDefaultProps(props, defaultProps);\r\n            }\r\n            const env = (parent && parent.childEnv) || app.env;\r\n            this.childEnv = env;\r\n            for (const key in props) {\r\n                const prop = props[key];\r\n                if (prop && typeof prop === \"object\" && targets.has(prop)) {\r\n                    props[key] = useState(prop);\r\n                }\r\n            }\r\n            this.component = new C(props, env, this);\r\n            const ctx = Object.assign(Object.create(this.component), { this: this.component });\r\n            this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);\r\n            this.component.setup();\r\n            currentNode = null;\r\n        }\r\n        mountComponent(target, options) {\r\n            const fiber = new MountFiber(this, target, options);\r\n            this.app.scheduler.addFiber(fiber);\r\n            this.initiateRender(fiber);\r\n        }\r\n        async initiateRender(fiber) {\r\n            this.fiber = fiber;\r\n            if (this.mounted.length) {\r\n                fiber.root.mounted.push(fiber);\r\n            }\r\n            const component = this.component;\r\n            try {\r\n                await Promise.all(this.willStart.map((f) => f.call(component)));\r\n            }\r\n            catch (e) {\r\n                this.app.handleError({ node: this, error: e });\r\n                return;\r\n            }\r\n            if (this.status === 0 /* NEW */ && this.fiber === fiber) {\r\n                fiber.render();\r\n            }\r\n        }\r\n        async render(deep) {\r\n            if (this.status >= 2 /* CANCELLED */) {\r\n                return;\r\n            }\r\n            let current = this.fiber;\r\n            if (current && (current.root.locked || current.bdom === true)) {\r\n                await Promise.resolve();\r\n                // situation may have changed after the microtask tick\r\n                current = this.fiber;\r\n            }\r\n            if (current) {\r\n                if (!current.bdom && !fibersInError.has(current)) {\r\n                    if (deep) {\r\n                        // we want the render from this point on to be with deep=true\r\n                        current.deep = deep;\r\n                    }\r\n                    return;\r\n                }\r\n                // if current rendering was with deep=true, we want this one to be the same\r\n                deep = deep || current.deep;\r\n            }\r\n            else if (!this.bdom) {\r\n                return;\r\n            }\r\n            const fiber = makeRootFiber(this);\r\n            fiber.deep = deep;\r\n            this.fiber = fiber;\r\n            this.app.scheduler.addFiber(fiber);\r\n            await Promise.resolve();\r\n            if (this.status >= 2 /* CANCELLED */) {\r\n                return;\r\n            }\r\n            // We only want to actually render the component if the following two\r\n            // conditions are true:\r\n            // * this.fiber: it could be null, in which case the render has been cancelled\r\n            // * (current || !fiber.parent): if current is not null, this means that the\r\n            //   render function was called when a render was already occurring. In this\r\n            //   case, the pending rendering was cancelled, and the fiber needs to be\r\n            //   rendered to complete the work.  If current is null, we check that the\r\n            //   fiber has no parent.  If that is the case, the fiber was downgraded from\r\n            //   a root fiber to a child fiber in the previous microtick, because it was\r\n            //   embedded in a rendering coming from above, so the fiber will be rendered\r\n            //   in the next microtick anyway, so we should not render it again.\r\n            if (this.fiber === fiber && (current || !fiber.parent)) {\r\n                fiber.render();\r\n            }\r\n        }\r\n        cancel() {\r\n            this._cancel();\r\n            delete this.parent.children[this.parentKey];\r\n            this.app.scheduler.scheduleDestroy(this);\r\n        }\r\n        _cancel() {\r\n            this.status = 2 /* CANCELLED */;\r\n            const children = this.children;\r\n            for (let childKey in children) {\r\n                children[childKey]._cancel();\r\n            }\r\n        }\r\n        destroy() {\r\n            let shouldRemove = this.status === 1 /* MOUNTED */;\r\n            this._destroy();\r\n            if (shouldRemove) {\r\n                this.bdom.remove();\r\n            }\r\n        }\r\n        _destroy() {\r\n            const component = this.component;\r\n            if (this.status === 1 /* MOUNTED */) {\r\n                for (let cb of this.willUnmount) {\r\n                    cb.call(component);\r\n                }\r\n            }\r\n            for (let child of Object.values(this.children)) {\r\n                child._destroy();\r\n            }\r\n            if (this.willDestroy.length) {\r\n                try {\r\n                    for (let cb of this.willDestroy) {\r\n                        cb.call(component);\r\n                    }\r\n                }\r\n                catch (e) {\r\n                    this.app.handleError({ error: e, node: this });\r\n                }\r\n            }\r\n            this.status = 3 /* DESTROYED */;\r\n        }\r\n        async updateAndRender(props, parentFiber) {\r\n            this.nextProps = props;\r\n            props = Object.assign({}, props);\r\n            // update\r\n            const fiber = makeChildFiber(this, parentFiber);\r\n            this.fiber = fiber;\r\n            const component = this.component;\r\n            const defaultProps = component.constructor.defaultProps;\r\n            if (defaultProps) {\r\n                applyDefaultProps(props, defaultProps);\r\n            }\r\n            currentNode = this;\r\n            for (const key in props) {\r\n                const prop = props[key];\r\n                if (prop && typeof prop === \"object\" && targets.has(prop)) {\r\n                    props[key] = useState(prop);\r\n                }\r\n            }\r\n            currentNode = null;\r\n            const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));\r\n            await prom;\r\n            if (fiber !== this.fiber) {\r\n                return;\r\n            }\r\n            component.props = props;\r\n            fiber.render();\r\n            const parentRoot = parentFiber.root;\r\n            if (this.willPatch.length) {\r\n                parentRoot.willPatch.push(fiber);\r\n            }\r\n            if (this.patched.length) {\r\n                parentRoot.patched.push(fiber);\r\n            }\r\n        }\r\n        /**\r\n         * Finds a child that has dom that is not yet updated, and update it. This\r\n         * method is meant to be used only in the context of repatching the dom after\r\n         * a mounted hook failed and was handled.\r\n         */\r\n        updateDom() {\r\n            if (!this.fiber) {\r\n                return;\r\n            }\r\n            if (this.bdom === this.fiber.bdom) {\r\n                // If the error was handled by some child component, we need to find it to\r\n                // apply its change\r\n                for (let k in this.children) {\r\n                    const child = this.children[k];\r\n                    child.updateDom();\r\n                }\r\n            }\r\n            else {\r\n                // if we get here, this is the component that handled the error and rerendered\r\n                // itself, so we can simply patch the dom\r\n                this.bdom.patch(this.fiber.bdom, false);\r\n                this.fiber.appliedToDom = true;\r\n                this.fiber = null;\r\n            }\r\n        }\r\n        /**\r\n         * Sets a ref to a given HTMLElement.\r\n         *\r\n         * @param name the name of the ref to set\r\n         * @param el the HTMLElement to set the ref to. The ref is not set if the el\r\n         *  is null, but useRef will not return elements that are not in the DOM\r\n         */\r\n        setRef(name, el) {\r\n            if (el) {\r\n                this.refs[name] = el;\r\n            }\r\n        }\r\n        // ---------------------------------------------------------------------------\r\n        // Block DOM methods\r\n        // ---------------------------------------------------------------------------\r\n        firstNode() {\r\n            const bdom = this.bdom;\r\n            return bdom ? bdom.firstNode() : undefined;\r\n        }\r\n        mount(parent, anchor) {\r\n            const bdom = this.fiber.bdom;\r\n            this.bdom = bdom;\r\n            bdom.mount(parent, anchor);\r\n            this.status = 1 /* MOUNTED */;\r\n            this.fiber.appliedToDom = true;\r\n            this.children = this.fiber.childrenMap;\r\n            this.fiber = null;\r\n        }\r\n        moveBeforeDOMNode(node, parent) {\r\n            this.bdom.moveBeforeDOMNode(node, parent);\r\n        }\r\n        moveBeforeVNode(other, afterNode) {\r\n            this.bdom.moveBeforeVNode(other ? other.bdom : null, afterNode);\r\n        }\r\n        patch() {\r\n            if (this.fiber && this.fiber.parent) {\r\n                // we only patch here renderings coming from above. renderings initiated\r\n                // by the component will be patched independently in the appropriate\r\n                // fiber.complete\r\n                this._patch();\r\n                this.props = this.nextProps;\r\n            }\r\n        }\r\n        _patch() {\r\n            let hasChildren = false;\r\n            // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n            for (let _k in this.children) {\r\n                hasChildren = true;\r\n                break;\r\n            }\r\n            const fiber = this.fiber;\r\n            this.children = fiber.childrenMap;\r\n            this.bdom.patch(fiber.bdom, hasChildren);\r\n            fiber.appliedToDom = true;\r\n            this.fiber = null;\r\n        }\r\n        beforeRemove() {\r\n            this._destroy();\r\n        }\r\n        remove() {\r\n            this.bdom.remove();\r\n        }\r\n        // ---------------------------------------------------------------------------\r\n        // Some debug helpers\r\n        // ---------------------------------------------------------------------------\r\n        get name() {\r\n            return this.component.constructor.name;\r\n        }\r\n        get subscriptions() {\r\n            const render = batchedRenderFunctions.get(this);\r\n            return render ? getSubscriptions(render) : [];\r\n        }\r\n    }\r\n\r\n    const TIMEOUT = Symbol(\"timeout\");\r\n    const HOOK_TIMEOUT = {\r\n        onWillStart: 3000,\r\n        onWillUpdateProps: 3000,\r\n    };\r\n    function wrapError(fn, hookName) {\r\n        const error = new OwlError();\r\n        const timeoutError = new OwlError();\r\n        const node = getCurrent();\r\n        return (...args) => {\r\n            const onError = (cause) => {\r\n                error.cause = cause;\r\n                error.message =\r\n                    cause instanceof Error\r\n                        ? `The following error occurred in ${hookName}: \"${cause.message}\"`\r\n                        : `Something that is not an Error was thrown in ${hookName} (see this Error's \"cause\" property)`;\r\n                throw error;\r\n            };\r\n            let result;\r\n            try {\r\n                result = fn(...args);\r\n            }\r\n            catch (cause) {\r\n                onError(cause);\r\n            }\r\n            if (!(result instanceof Promise)) {\r\n                return result;\r\n            }\r\n            const timeout = HOOK_TIMEOUT[hookName];\r\n            if (timeout) {\r\n                const fiber = node.fiber;\r\n                Promise.race([\r\n                    result.catch(() => { }),\r\n                    new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), timeout)),\r\n                ]).then((res) => {\r\n                    if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {\r\n                        timeoutError.message = `${hookName}'s promise hasn't resolved after ${timeout / 1000} seconds`;\r\n                        console.log(timeoutError);\r\n                    }\r\n                });\r\n            }\r\n            return result.catch(onError);\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    //  hooks\r\n    // -----------------------------------------------------------------------------\r\n    function onWillStart(fn) {\r\n        const node = getCurrent();\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        node.willStart.push(decorate(fn.bind(node.component), \"onWillStart\"));\r\n    }\r\n    function onWillUpdateProps(fn) {\r\n        const node = getCurrent();\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        node.willUpdateProps.push(decorate(fn.bind(node.component), \"onWillUpdateProps\"));\r\n    }\r\n    function onMounted(fn) {\r\n        const node = getCurrent();\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        node.mounted.push(decorate(fn.bind(node.component), \"onMounted\"));\r\n    }\r\n    function onWillPatch(fn) {\r\n        const node = getCurrent();\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        node.willPatch.unshift(decorate(fn.bind(node.component), \"onWillPatch\"));\r\n    }\r\n    function onPatched(fn) {\r\n        const node = getCurrent();\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        node.patched.push(decorate(fn.bind(node.component), \"onPatched\"));\r\n    }\r\n    function onWillUnmount(fn) {\r\n        const node = getCurrent();\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        node.willUnmount.unshift(decorate(fn.bind(node.component), \"onWillUnmount\"));\r\n    }\r\n    function onWillDestroy(fn) {\r\n        const node = getCurrent();\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        node.willDestroy.push(decorate(fn.bind(node.component), \"onWillDestroy\"));\r\n    }\r\n    function onWillRender(fn) {\r\n        const node = getCurrent();\r\n        const renderFn = node.renderFn;\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        fn = decorate(fn.bind(node.component), \"onWillRender\");\r\n        node.renderFn = () => {\r\n            fn();\r\n            return renderFn();\r\n        };\r\n    }\r\n    function onRendered(fn) {\r\n        const node = getCurrent();\r\n        const renderFn = node.renderFn;\r\n        const decorate = node.app.dev ? wrapError : (fn) => fn;\r\n        fn = decorate(fn.bind(node.component), \"onRendered\");\r\n        node.renderFn = () => {\r\n            const result = renderFn();\r\n            fn();\r\n            return result;\r\n        };\r\n    }\r\n    function onError(callback) {\r\n        const node = getCurrent();\r\n        let handlers = nodeErrorHandlers.get(node);\r\n        if (!handlers) {\r\n            handlers = [];\r\n            nodeErrorHandlers.set(node, handlers);\r\n        }\r\n        handlers.push(callback.bind(node.component));\r\n    }\r\n\r\n    class Component {\r\n        constructor(props, env, node) {\r\n            this.props = props;\r\n            this.env = env;\r\n            this.__owl__ = node;\r\n        }\r\n        setup() { }\r\n        render(deep = false) {\r\n            this.__owl__.render(deep === true);\r\n        }\r\n    }\r\n    Component.template = \"\";\r\n\r\n    const VText = text(\"\").constructor;\r\n    class VPortal extends VText {\r\n        constructor(selector, content) {\r\n            super(\"\");\r\n            this.target = null;\r\n            this.selector = selector;\r\n            this.content = content;\r\n        }\r\n        mount(parent, anchor) {\r\n            super.mount(parent, anchor);\r\n            this.target = document.querySelector(this.selector);\r\n            if (this.target) {\r\n                this.content.mount(this.target, null);\r\n            }\r\n            else {\r\n                this.content.mount(parent, anchor);\r\n            }\r\n        }\r\n        beforeRemove() {\r\n            this.content.beforeRemove();\r\n        }\r\n        remove() {\r\n            if (this.content) {\r\n                super.remove();\r\n                this.content.remove();\r\n                this.content = null;\r\n            }\r\n        }\r\n        patch(other) {\r\n            super.patch(other);\r\n            if (this.content) {\r\n                this.content.patch(other.content, true);\r\n            }\r\n            else {\r\n                this.content = other.content;\r\n                this.content.mount(this.target, null);\r\n            }\r\n        }\r\n    }\r\n    /**\r\n     * kind of similar to <t t-slot=\"default\"/>, but it wraps it around a VPortal\r\n     */\r\n    function portalTemplate(app, bdom, helpers) {\r\n        let { callSlot } = helpers;\r\n        return function template(ctx, node, key = \"\") {\r\n            return new VPortal(ctx.props.target, callSlot(ctx, node, key, \"default\", false, null));\r\n        };\r\n    }\r\n    class Portal extends Component {\r\n        setup() {\r\n            const node = this.__owl__;\r\n            onMounted(() => {\r\n                const portal = node.bdom;\r\n                if (!portal.target) {\r\n                    const target = document.querySelector(this.props.target);\r\n                    if (target) {\r\n                        portal.content.moveBeforeDOMNode(target.firstChild, target);\r\n                    }\r\n                    else {\r\n                        throw new OwlError(\"invalid portal target\");\r\n                    }\r\n                }\r\n            });\r\n            onWillUnmount(() => {\r\n                const portal = node.bdom;\r\n                portal.remove();\r\n            });\r\n        }\r\n    }\r\n    Portal.template = \"__portal__\";\r\n    Portal.props = {\r\n        target: {\r\n            type: String,\r\n        },\r\n        slots: true,\r\n    };\r\n\r\n    // -----------------------------------------------------------------------------\r\n    // helpers\r\n    // -----------------------------------------------------------------------------\r\n    const isUnionType = (t) => Array.isArray(t);\r\n    const isBaseType = (t) => typeof t !== \"object\";\r\n    const isValueType = (t) => typeof t === \"object\" && t && \"value\" in t;\r\n    function isOptional(t) {\r\n        return typeof t === \"object\" && \"optional\" in t ? t.optional || false : false;\r\n    }\r\n    function describeType(type) {\r\n        return type === \"*\" || type === true ? \"value\" : type.name.toLowerCase();\r\n    }\r\n    function describe(info) {\r\n        if (isBaseType(info)) {\r\n            return describeType(info);\r\n        }\r\n        else if (isUnionType(info)) {\r\n            return info.map(describe).join(\" or \");\r\n        }\r\n        else if (isValueType(info)) {\r\n            return String(info.value);\r\n        }\r\n        if (\"element\" in info) {\r\n            return `list of ${describe({ type: info.element, optional: false })}s`;\r\n        }\r\n        if (\"shape\" in info) {\r\n            return `object`;\r\n        }\r\n        return describe(info.type || \"*\");\r\n    }\r\n    function toSchema(spec) {\r\n        return Object.fromEntries(spec.map((e) => e.endsWith(\"?\") ? [e.slice(0, -1), { optional: true }] : [e, { type: \"*\", optional: false }]));\r\n    }\r\n    /**\r\n     * Main validate function\r\n     */\r\n    function validate(obj, spec) {\r\n        let errors = validateSchema(obj, spec);\r\n        if (errors.length) {\r\n            throw new OwlError(\"Invalid object: \" + errors.join(\", \"));\r\n        }\r\n    }\r\n    /**\r\n     * Helper validate function, to get the list of errors. useful if one want to\r\n     * manipulate the errors without parsing an error object\r\n     */\r\n    function validateSchema(obj, schema) {\r\n        if (Array.isArray(schema)) {\r\n            schema = toSchema(schema);\r\n        }\r\n        obj = toRaw(obj);\r\n        let errors = [];\r\n        // check if each value in obj has correct shape\r\n        for (let key in obj) {\r\n            if (key in schema) {\r\n                let result = validateType(key, obj[key], schema[key]);\r\n                if (result) {\r\n                    errors.push(result);\r\n                }\r\n            }\r\n            else if (!(\"*\" in schema)) {\r\n                errors.push(`unknown key '${key}'`);\r\n            }\r\n        }\r\n        // check that all specified keys are defined in obj\r\n        for (let key in schema) {\r\n            const spec = schema[key];\r\n            if (key !== \"*\" && !isOptional(spec) && !(key in obj)) {\r\n                const isObj = typeof spec === \"object\" && !Array.isArray(spec);\r\n                const isAny = spec === \"*\" || (isObj && \"type\" in spec ? spec.type === \"*\" : isObj);\r\n                let detail = isAny ? \"\" : ` (should be a ${describe(spec)})`;\r\n                errors.push(`'${key}' is missing${detail}`);\r\n            }\r\n        }\r\n        return errors;\r\n    }\r\n    function validateBaseType(key, value, type) {\r\n        if (typeof type === \"function\") {\r\n            if (typeof value === \"object\") {\r\n                if (!(value instanceof type)) {\r\n                    return `'${key}' is not a ${describeType(type)}`;\r\n                }\r\n            }\r\n            else if (typeof value !== type.name.toLowerCase()) {\r\n                return `'${key}' is not a ${describeType(type)}`;\r\n            }\r\n        }\r\n        return null;\r\n    }\r\n    function validateArrayType(key, value, descr) {\r\n        if (!Array.isArray(value)) {\r\n            return `'${key}' is not a list of ${describe(descr)}s`;\r\n        }\r\n        for (let i = 0; i < value.length; i++) {\r\n            const error = validateType(`${key}[${i}]`, value[i], descr);\r\n            if (error) {\r\n                return error;\r\n            }\r\n        }\r\n        return null;\r\n    }\r\n    function validateType(key, value, descr) {\r\n        if (value === undefined) {\r\n            return isOptional(descr) ? null : `'${key}' is undefined (should be a ${describe(descr)})`;\r\n        }\r\n        else if (isBaseType(descr)) {\r\n            return validateBaseType(key, value, descr);\r\n        }\r\n        else if (isValueType(descr)) {\r\n            return value === descr.value ? null : `'${key}' is not equal to '${descr.value}'`;\r\n        }\r\n        else if (isUnionType(descr)) {\r\n            let validDescr = descr.find((p) => !validateType(key, value, p));\r\n            return validDescr ? null : `'${key}' is not a ${describe(descr)}`;\r\n        }\r\n        let result = null;\r\n        if (\"element\" in descr) {\r\n            result = validateArrayType(key, value, descr.element);\r\n        }\r\n        else if (\"shape\" in descr) {\r\n            if (typeof value !== \"object\" || Array.isArray(value)) {\r\n                result = `'${key}' is not an object`;\r\n            }\r\n            else {\r\n                const errors = validateSchema(value, descr.shape);\r\n                if (errors.length) {\r\n                    result = `'${key}' doesn't have the correct shape (${errors.join(\", \")})`;\r\n                }\r\n            }\r\n        }\r\n        else if (\"values\" in descr) {\r\n            if (typeof value !== \"object\" || Array.isArray(value)) {\r\n                result = `'${key}' is not an object`;\r\n            }\r\n            else {\r\n                const errors = Object.entries(value)\r\n                    .map(([key, value]) => validateType(key, value, descr.values))\r\n                    .filter(Boolean);\r\n                if (errors.length) {\r\n                    result = `some of the values in '${key}' are invalid (${errors.join(\", \")})`;\r\n                }\r\n            }\r\n        }\r\n        if (\"type\" in descr && !result) {\r\n            result = validateType(key, value, descr.type);\r\n        }\r\n        if (\"validate\" in descr && !result) {\r\n            result = !descr.validate(value) ? `'${key}' is not valid` : null;\r\n        }\r\n        return result;\r\n    }\r\n\r\n    const ObjectCreate = Object.create;\r\n    /**\r\n     * This file contains utility functions that will be injected in each template,\r\n     * to perform various useful tasks in the compiled code.\r\n     */\r\n    function withDefault(value, defaultValue) {\r\n        return value === undefined || value === null || value === false ? defaultValue : value;\r\n    }\r\n    function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {\r\n        key = key + \"__slot_\" + name;\r\n        const slots = ctx.props.slots || {};\r\n        const { __render, __ctx, __scope } = slots[name] || {};\r\n        const slotScope = ObjectCreate(__ctx || {});\r\n        if (__scope) {\r\n            slotScope[__scope] = extra;\r\n        }\r\n        const slotBDom = __render ? __render(slotScope, parent, key) : null;\r\n        if (defaultContent) {\r\n            let child1 = undefined;\r\n            let child2 = undefined;\r\n            if (slotBDom) {\r\n                child1 = dynamic ? toggler(name, slotBDom) : slotBDom;\r\n            }\r\n            else {\r\n                child2 = defaultContent(ctx, parent, key);\r\n            }\r\n            return multi([child1, child2]);\r\n        }\r\n        return slotBDom || text(\"\");\r\n    }\r\n    function capture(ctx) {\r\n        const result = ObjectCreate(ctx);\r\n        for (let k in ctx) {\r\n            result[k] = ctx[k];\r\n        }\r\n        return result;\r\n    }\r\n    function withKey(elem, k) {\r\n        elem.key = k;\r\n        return elem;\r\n    }\r\n    function prepareList(collection) {\r\n        let keys;\r\n        let values;\r\n        if (Array.isArray(collection)) {\r\n            keys = collection;\r\n            values = collection;\r\n        }\r\n        else if (collection instanceof Map) {\r\n            keys = [...collection.keys()];\r\n            values = [...collection.values()];\r\n        }\r\n        else if (Symbol.iterator in Object(collection)) {\r\n            keys = [...collection];\r\n            values = keys;\r\n        }\r\n        else if (collection && typeof collection === \"object\") {\r\n            values = Object.values(collection);\r\n            keys = Object.keys(collection);\r\n        }\r\n        else {\r\n            throw new OwlError(`Invalid loop expression: \"${collection}\" is not iterable`);\r\n        }\r\n        const n = values.length;\r\n        return [keys, values, n, new Array(n)];\r\n    }\r\n    const isBoundary = Symbol(\"isBoundary\");\r\n    function setContextValue(ctx, key, value) {\r\n        const ctx0 = ctx;\r\n        while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {\r\n            const newCtx = ctx.__proto__;\r\n            if (!newCtx) {\r\n                ctx = ctx0;\r\n                break;\r\n            }\r\n            ctx = newCtx;\r\n        }\r\n        ctx[key] = value;\r\n    }\r\n    function toNumber(val) {\r\n        const n = parseFloat(val);\r\n        return isNaN(n) ? val : n;\r\n    }\r\n    function shallowEqual(l1, l2) {\r\n        for (let i = 0, l = l1.length; i < l; i++) {\r\n            if (l1[i] !== l2[i]) {\r\n                return false;\r\n            }\r\n        }\r\n        return true;\r\n    }\r\n    class LazyValue {\r\n        constructor(fn, ctx, component, node, key) {\r\n            this.fn = fn;\r\n            this.ctx = capture(ctx);\r\n            this.component = component;\r\n            this.node = node;\r\n            this.key = key;\r\n        }\r\n        evaluate() {\r\n            return this.fn.call(this.component, this.ctx, this.node, this.key);\r\n        }\r\n        toString() {\r\n            return this.evaluate().toString();\r\n        }\r\n    }\r\n    /*\r\n     * Safely outputs `value` as a block depending on the nature of `value`\r\n     */\r\n    function safeOutput(value, defaultValue) {\r\n        if (value === undefined || value === null) {\r\n            return defaultValue ? toggler(\"default\", defaultValue) : toggler(\"undefined\", text(\"\"));\r\n        }\r\n        let safeKey;\r\n        let block;\r\n        switch (typeof value) {\r\n            case \"object\":\r\n                if (value instanceof Markup) {\r\n                    safeKey = `string_safe`;\r\n                    block = html(value);\r\n                }\r\n                else if (value instanceof LazyValue) {\r\n                    safeKey = `lazy_value`;\r\n                    block = value.evaluate();\r\n                }\r\n                else if (value instanceof String) {\r\n                    safeKey = \"string_unsafe\";\r\n                    block = text(value);\r\n                }\r\n                else {\r\n                    // Assuming it is a block\r\n                    safeKey = \"block_safe\";\r\n                    block = value;\r\n                }\r\n                break;\r\n            case \"string\":\r\n                safeKey = \"string_unsafe\";\r\n                block = text(value);\r\n                break;\r\n            default:\r\n                safeKey = \"string_unsafe\";\r\n                block = text(String(value));\r\n        }\r\n        return toggler(safeKey, block);\r\n    }\r\n    /**\r\n     * Validate the component props (or next props) against the (static) props\r\n     * description.  This is potentially an expensive operation: it may needs to\r\n     * visit recursively the props and all the children to check if they are valid.\r\n     * This is why it is only done in 'dev' mode.\r\n     */\r\n    function validateProps(name, props, comp) {\r\n        const ComponentClass = typeof name !== \"string\"\r\n            ? name\r\n            : comp.constructor.components[name];\r\n        if (!ComponentClass) {\r\n            // this is an error, wrong component. We silently return here instead so the\r\n            // error is triggered by the usual path ('component' function)\r\n            return;\r\n        }\r\n        const schema = ComponentClass.props;\r\n        if (!schema) {\r\n            if (comp.__owl__.app.warnIfNoStaticProps) {\r\n                console.warn(`Component '${ComponentClass.name}' does not have a static props description`);\r\n            }\r\n            return;\r\n        }\r\n        const defaultProps = ComponentClass.defaultProps;\r\n        if (defaultProps) {\r\n            let isMandatory = (name) => Array.isArray(schema)\r\n                ? schema.includes(name)\r\n                : name in schema && !(\"*\" in schema) && !isOptional(schema[name]);\r\n            for (let p in defaultProps) {\r\n                if (isMandatory(p)) {\r\n                    throw new OwlError(`A default value cannot be defined for a mandatory prop (name: '${p}', component: ${ComponentClass.name})`);\r\n                }\r\n            }\r\n        }\r\n        const errors = validateSchema(props, schema);\r\n        if (errors.length) {\r\n            throw new OwlError(`Invalid props for component '${ComponentClass.name}': ` + errors.join(\", \"));\r\n        }\r\n    }\r\n    function makeRefWrapper(node) {\r\n        let refNames = new Set();\r\n        return (name, fn) => {\r\n            if (refNames.has(name)) {\r\n                throw new OwlError(`Cannot set the same ref more than once in the same component, ref \"${name}\" was set multiple times in ${node.name}`);\r\n            }\r\n            refNames.add(name);\r\n            return fn;\r\n        };\r\n    }\r\n    const helpers = {\r\n        withDefault,\r\n        zero: Symbol(\"zero\"),\r\n        isBoundary,\r\n        callSlot,\r\n        capture,\r\n        withKey,\r\n        prepareList,\r\n        setContextValue,\r\n        shallowEqual,\r\n        toNumber,\r\n        validateProps,\r\n        LazyValue,\r\n        safeOutput,\r\n        createCatcher,\r\n        markRaw,\r\n        OwlError,\r\n        makeRefWrapper,\r\n    };\r\n\r\n    /**\r\n     * Parses an XML string into an XML document, throwing errors on parser errors\r\n     * instead of returning an XML document containing the parseerror.\r\n     *\r\n     * @param xml the string to parse\r\n     * @returns an XML document corresponding to the content of the string\r\n     */\r\n    function parseXML(xml) {\r\n        const parser = new DOMParser();\r\n        const doc = parser.parseFromString(xml, \"text/xml\");\r\n        if (doc.getElementsByTagName(\"parsererror\").length) {\r\n            let msg = \"Invalid XML in template.\";\r\n            const parsererrorText = doc.getElementsByTagName(\"parsererror\")[0].textContent;\r\n            if (parsererrorText) {\r\n                msg += \"\\nThe parser has produced the following error message:\\n\" + parsererrorText;\r\n                const re = /\\d+/g;\r\n                const firstMatch = re.exec(parsererrorText);\r\n                if (firstMatch) {\r\n                    const lineNumber = Number(firstMatch[0]);\r\n                    const line = xml.split(\"\\n\")[lineNumber - 1];\r\n                    const secondMatch = re.exec(parsererrorText);\r\n                    if (line && secondMatch) {\r\n                        const columnIndex = Number(secondMatch[0]) - 1;\r\n                        if (line[columnIndex]) {\r\n                            msg +=\r\n                                `\\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\\n` +\r\n                                    `${line}\\n${\"-\".repeat(columnIndex - 1)}^`;\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n            throw new OwlError(msg);\r\n        }\r\n        return doc;\r\n    }\r\n\r\n    const bdom = { text, createBlock, list, multi, html, toggler, comment };\r\n    class TemplateSet {\r\n        constructor(config = {}) {\r\n            this.rawTemplates = Object.create(globalTemplates);\r\n            this.templates = {};\r\n            this.Portal = Portal;\r\n            this.dev = config.dev || false;\r\n            this.translateFn = config.translateFn;\r\n            this.translatableAttributes = config.translatableAttributes;\r\n            if (config.templates) {\r\n                if (config.templates instanceof Document || typeof config.templates === \"string\") {\r\n                    this.addTemplates(config.templates);\r\n                }\r\n                else {\r\n                    for (const name in config.templates) {\r\n                        this.addTemplate(name, config.templates[name]);\r\n                    }\r\n                }\r\n            }\r\n            this.getRawTemplate = config.getTemplate;\r\n        }\r\n        static registerTemplate(name, fn) {\r\n            globalTemplates[name] = fn;\r\n        }\r\n        addTemplate(name, template) {\r\n            if (name in this.rawTemplates) {\r\n                // this check can be expensive, just silently ignore double definitions outside dev mode\r\n                if (!this.dev) {\r\n                    return;\r\n                }\r\n                const rawTemplate = this.rawTemplates[name];\r\n                const currentAsString = typeof rawTemplate === \"string\"\r\n                    ? rawTemplate\r\n                    : rawTemplate instanceof Element\r\n                        ? rawTemplate.outerHTML\r\n                        : rawTemplate.toString();\r\n                const newAsString = typeof template === \"string\" ? template : template.outerHTML;\r\n                if (currentAsString === newAsString) {\r\n                    return;\r\n                }\r\n                throw new OwlError(`Template ${name} already defined with different content`);\r\n            }\r\n            this.rawTemplates[name] = template;\r\n        }\r\n        addTemplates(xml) {\r\n            if (!xml) {\r\n                // empty string\r\n                return;\r\n            }\r\n            xml = xml instanceof Document ? xml : parseXML(xml);\r\n            for (const template of xml.querySelectorAll(\"[t-name]\")) {\r\n                const name = template.getAttribute(\"t-name\");\r\n                this.addTemplate(name, template);\r\n            }\r\n        }\r\n        getTemplate(name) {\r\n            var _a;\r\n            if (!(name in this.templates)) {\r\n                const rawTemplate = ((_a = this.getRawTemplate) === null || _a === void 0 ? void 0 : _a.call(this, name)) || this.rawTemplates[name];\r\n                if (rawTemplate === undefined) {\r\n                    let extraInfo = \"\";\r\n                    try {\r\n                        const componentName = getCurrent().component.constructor.name;\r\n                        extraInfo = ` (for component \"${componentName}\")`;\r\n                    }\r\n                    catch { }\r\n                    throw new OwlError(`Missing template: \"${name}\"${extraInfo}`);\r\n                }\r\n                const isFn = typeof rawTemplate === \"function\" && !(rawTemplate instanceof Element);\r\n                const templateFn = isFn ? rawTemplate : this._compileTemplate(name, rawTemplate);\r\n                // first add a function to lazily get the template, in case there is a\r\n                // recursive call to the template name\r\n                const templates = this.templates;\r\n                this.templates[name] = function (context, parent) {\r\n                    return templates[name].call(this, context, parent);\r\n                };\r\n                const template = templateFn(this, bdom, helpers);\r\n                this.templates[name] = template;\r\n            }\r\n            return this.templates[name];\r\n        }\r\n        _compileTemplate(name, template) {\r\n            throw new OwlError(`Unable to compile a template. Please use owl full build instead`);\r\n        }\r\n        callTemplate(owner, subTemplate, ctx, parent, key) {\r\n            const template = this.getTemplate(subTemplate);\r\n            return toggler(subTemplate, template.call(owner, ctx, parent, key + subTemplate));\r\n        }\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    //  xml tag helper\r\n    // -----------------------------------------------------------------------------\r\n    const globalTemplates = {};\r\n    function xml(...args) {\r\n        const name = `__template__${xml.nextId++}`;\r\n        const value = String.raw(...args);\r\n        globalTemplates[name] = value;\r\n        return name;\r\n    }\r\n    xml.nextId = 1;\r\n    TemplateSet.registerTemplate(\"__portal__\", portalTemplate);\r\n\r\n    /**\r\n     * Owl QWeb Expression Parser\r\n     *\r\n     * Owl needs in various contexts to be able to understand the structure of a\r\n     * string representing a javascript expression.  The usual goal is to be able\r\n     * to rewrite some variables.  For example, if a template has\r\n     *\r\n     *  ```xml\r\n     *  <t t-if=\"computeSomething({val: state.val})\">...</t>\r\n     * ```\r\n     *\r\n     * this needs to be translated in something like this:\r\n     *\r\n     * ```js\r\n     *   if (context[\"computeSomething\"]({val: context[\"state\"].val})) { ... }\r\n     * ```\r\n     *\r\n     * This file contains the implementation of an extremely naive tokenizer/parser\r\n     * and evaluator for javascript expressions.  The supported grammar is basically\r\n     * only expressive enough to understand the shape of objects, of arrays, and\r\n     * various operators.\r\n     */\r\n    //------------------------------------------------------------------------------\r\n    // Misc types, constants and helpers\r\n    //------------------------------------------------------------------------------\r\n    const RESERVED_WORDS = \"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date\".split(\",\");\r\n    const WORD_REPLACEMENT = Object.assign(Object.create(null), {\r\n        and: \"&&\",\r\n        or: \"||\",\r\n        gt: \">\",\r\n        gte: \">=\",\r\n        lt: \"<\",\r\n        lte: \"<=\",\r\n    });\r\n    const STATIC_TOKEN_MAP = Object.assign(Object.create(null), {\r\n        \"{\": \"LEFT_BRACE\",\r\n        \"}\": \"RIGHT_BRACE\",\r\n        \"[\": \"LEFT_BRACKET\",\r\n        \"]\": \"RIGHT_BRACKET\",\r\n        \":\": \"COLON\",\r\n        \",\": \"COMMA\",\r\n        \"(\": \"LEFT_PAREN\",\r\n        \")\": \"RIGHT_PAREN\",\r\n    });\r\n    // note that the space after typeof is relevant. It makes sure that the formatted\r\n    // expression has a space after typeof. Currently we don't support delete and void\r\n    const OPERATORS = \"...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ,new ,|,&,^,~\".split(\",\");\r\n    let tokenizeString = function (expr) {\r\n        let s = expr[0];\r\n        let start = s;\r\n        if (s !== \"'\" && s !== '\"' && s !== \"`\") {\r\n            return false;\r\n        }\r\n        let i = 1;\r\n        let cur;\r\n        while (expr[i] && expr[i] !== start) {\r\n            cur = expr[i];\r\n            s += cur;\r\n            if (cur === \"\\\\\") {\r\n                i++;\r\n                cur = expr[i];\r\n                if (!cur) {\r\n                    throw new OwlError(\"Invalid expression\");\r\n                }\r\n                s += cur;\r\n            }\r\n            i++;\r\n        }\r\n        if (expr[i] !== start) {\r\n            throw new OwlError(\"Invalid expression\");\r\n        }\r\n        s += start;\r\n        if (start === \"`\") {\r\n            return {\r\n                type: \"TEMPLATE_STRING\",\r\n                value: s,\r\n                replace(replacer) {\r\n                    return s.replace(/\\$\\{(.*?)\\}/g, (match, group) => {\r\n                        return \"${\" + replacer(group) + \"}\";\r\n                    });\r\n                },\r\n            };\r\n        }\r\n        return { type: \"VALUE\", value: s };\r\n    };\r\n    let tokenizeNumber = function (expr) {\r\n        let s = expr[0];\r\n        if (s && s.match(/[0-9]/)) {\r\n            let i = 1;\r\n            while (expr[i] && expr[i].match(/[0-9]|\\./)) {\r\n                s += expr[i];\r\n                i++;\r\n            }\r\n            return { type: \"VALUE\", value: s };\r\n        }\r\n        else {\r\n            return false;\r\n        }\r\n    };\r\n    let tokenizeSymbol = function (expr) {\r\n        let s = expr[0];\r\n        if (s && s.match(/[a-zA-Z_\\$]/)) {\r\n            let i = 1;\r\n            while (expr[i] && expr[i].match(/\\w/)) {\r\n                s += expr[i];\r\n                i++;\r\n            }\r\n            if (s in WORD_REPLACEMENT) {\r\n                return { type: \"OPERATOR\", value: WORD_REPLACEMENT[s], size: s.length };\r\n            }\r\n            return { type: \"SYMBOL\", value: s };\r\n        }\r\n        else {\r\n            return false;\r\n        }\r\n    };\r\n    const tokenizeStatic = function (expr) {\r\n        const char = expr[0];\r\n        if (char && char in STATIC_TOKEN_MAP) {\r\n            return { type: STATIC_TOKEN_MAP[char], value: char };\r\n        }\r\n        return false;\r\n    };\r\n    const tokenizeOperator = function (expr) {\r\n        for (let op of OPERATORS) {\r\n            if (expr.startsWith(op)) {\r\n                return { type: \"OPERATOR\", value: op };\r\n            }\r\n        }\r\n        return false;\r\n    };\r\n    const TOKENIZERS = [\r\n        tokenizeString,\r\n        tokenizeNumber,\r\n        tokenizeOperator,\r\n        tokenizeSymbol,\r\n        tokenizeStatic,\r\n    ];\r\n    /**\r\n     * Convert a javascript expression (as a string) into a list of tokens. For\r\n     * example: `tokenize(\"1 + b\")` will return:\r\n     * ```js\r\n     *  [\r\n     *   {type: \"VALUE\", value: \"1\"},\r\n     *   {type: \"OPERATOR\", value: \"+\"},\r\n     *   {type: \"SYMBOL\", value: \"b\"}\r\n     * ]\r\n     * ```\r\n     */\r\n    function tokenize(expr) {\r\n        const result = [];\r\n        let token = true;\r\n        let error;\r\n        let current = expr;\r\n        try {\r\n            while (token) {\r\n                current = current.trim();\r\n                if (current) {\r\n                    for (let tokenizer of TOKENIZERS) {\r\n                        token = tokenizer(current);\r\n                        if (token) {\r\n                            result.push(token);\r\n                            current = current.slice(token.size || token.value.length);\r\n                            break;\r\n                        }\r\n                    }\r\n                }\r\n                else {\r\n                    token = false;\r\n                }\r\n            }\r\n        }\r\n        catch (e) {\r\n            error = e; // Silence all errors and throw a generic error below\r\n        }\r\n        if (current.length || error) {\r\n            throw new OwlError(`Tokenizer error: could not tokenize \\`${expr}\\``);\r\n        }\r\n        return result;\r\n    }\r\n    //------------------------------------------------------------------------------\r\n    // Expression \"evaluator\"\r\n    //------------------------------------------------------------------------------\r\n    const isLeftSeparator = (token) => token && (token.type === \"LEFT_BRACE\" || token.type === \"COMMA\");\r\n    const isRightSeparator = (token) => token && (token.type === \"RIGHT_BRACE\" || token.type === \"COMMA\");\r\n    /**\r\n     * This is the main function exported by this file. This is the code that will\r\n     * process an expression (given as a string) and returns another expression with\r\n     * proper lookups in the context.\r\n     *\r\n     * Usually, this kind of code would be very simple to do if we had an AST (so,\r\n     * if we had a javascript parser), since then, we would only need to find the\r\n     * variables and replace them.  However, a parser is more complicated, and there\r\n     * are no standard builtin parser API.\r\n     *\r\n     * Since this method is applied to simple javasript expressions, and the work to\r\n     * be done is actually quite simple, we actually can get away with not using a\r\n     * parser, which helps with the code size.\r\n     *\r\n     * Here is the heuristic used by this method to determine if a token is a\r\n     * variable:\r\n     * - by default, all symbols are considered a variable\r\n     * - unless the previous token is a dot (in that case, this is a property: `a.b`)\r\n     * - or if the previous token is a left brace or a comma, and the next token is\r\n     *   a colon (in that case, this is an object key: `{a: b}`)\r\n     *\r\n     * Some specific code is also required to support arrow functions. If we detect\r\n     * the arrow operator, then we add the current (or some previous tokens) token to\r\n     * the list of variables so it does not get replaced by a lookup in the context\r\n     */\r\n    function compileExprToArray(expr) {\r\n        const localVars = new Set();\r\n        const tokens = tokenize(expr);\r\n        let i = 0;\r\n        let stack = []; // to track last opening (, [ or {\r\n        while (i < tokens.length) {\r\n            let token = tokens[i];\r\n            let prevToken = tokens[i - 1];\r\n            let nextToken = tokens[i + 1];\r\n            let groupType = stack[stack.length - 1];\r\n            switch (token.type) {\r\n                case \"LEFT_BRACE\":\r\n                case \"LEFT_BRACKET\":\r\n                case \"LEFT_PAREN\":\r\n                    stack.push(token.type);\r\n                    break;\r\n                case \"RIGHT_BRACE\":\r\n                case \"RIGHT_BRACKET\":\r\n                case \"RIGHT_PAREN\":\r\n                    stack.pop();\r\n            }\r\n            let isVar = token.type === \"SYMBOL\" && !RESERVED_WORDS.includes(token.value);\r\n            if (token.type === \"SYMBOL\" && !RESERVED_WORDS.includes(token.value)) {\r\n                if (prevToken) {\r\n                    // normalize missing tokens: {a} should be equivalent to {a:a}\r\n                    if (groupType === \"LEFT_BRACE\" &&\r\n                        isLeftSeparator(prevToken) &&\r\n                        isRightSeparator(nextToken)) {\r\n                        tokens.splice(i + 1, 0, { type: \"COLON\", value: \":\" }, { ...token });\r\n                        nextToken = tokens[i + 1];\r\n                    }\r\n                    if (prevToken.type === \"OPERATOR\" && prevToken.value === \".\") {\r\n                        isVar = false;\r\n                    }\r\n                    else if (prevToken.type === \"LEFT_BRACE\" || prevToken.type === \"COMMA\") {\r\n                        if (nextToken && nextToken.type === \"COLON\") {\r\n                            isVar = false;\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n            if (token.type === \"TEMPLATE_STRING\") {\r\n                token.value = token.replace((expr) => compileExpr(expr));\r\n            }\r\n            if (nextToken && nextToken.type === \"OPERATOR\" && nextToken.value === \"=>\") {\r\n                if (token.type === \"RIGHT_PAREN\") {\r\n                    let j = i - 1;\r\n                    while (j > 0 && tokens[j].type !== \"LEFT_PAREN\") {\r\n                        if (tokens[j].type === \"SYMBOL\" && tokens[j].originalValue) {\r\n                            tokens[j].value = tokens[j].originalValue;\r\n                            localVars.add(tokens[j].value); //] = { id: tokens[j].value, expr: tokens[j].value };\r\n                        }\r\n                        j--;\r\n                    }\r\n                }\r\n                else {\r\n                    localVars.add(token.value); //] = { id: token.value, expr: token.value };\r\n                }\r\n            }\r\n            if (isVar) {\r\n                token.varName = token.value;\r\n                if (!localVars.has(token.value)) {\r\n                    token.originalValue = token.value;\r\n                    token.value = `ctx['${token.value}']`;\r\n                }\r\n            }\r\n            i++;\r\n        }\r\n        // Mark all variables that have been used locally.\r\n        // This assumes the expression has only one scope (incorrect but \"good enough for now\")\r\n        for (const token of tokens) {\r\n            if (token.type === \"SYMBOL\" && token.varName && localVars.has(token.value)) {\r\n                token.originalValue = token.value;\r\n                token.value = `_${token.value}`;\r\n                token.isLocal = true;\r\n            }\r\n        }\r\n        return tokens;\r\n    }\r\n    // Leading spaces are trimmed during tokenization, so they need to be added back for some values\r\n    const paddedValues = new Map([[\"in \", \" in \"]]);\r\n    function compileExpr(expr) {\r\n        return compileExprToArray(expr)\r\n            .map((t) => paddedValues.get(t.value) || t.value)\r\n            .join(\"\");\r\n    }\r\n    const INTERP_REGEXP = /\\{\\{.*?\\}\\}|\\#\\{.*?\\}/g;\r\n    function replaceDynamicParts(s, replacer) {\r\n        let matches = s.match(INTERP_REGEXP);\r\n        if (matches && matches[0].length === s.length) {\r\n            return `(${replacer(s.slice(2, matches[0][0] === \"{\" ? -2 : -1))})`;\r\n        }\r\n        let r = s.replace(INTERP_REGEXP, (s) => \"${\" + replacer(s.slice(2, s[0] === \"{\" ? -2 : -1)) + \"}\");\r\n        return \"`\" + r + \"`\";\r\n    }\r\n    function interpolate(s) {\r\n        return replaceDynamicParts(s, compileExpr);\r\n    }\r\n\r\n    const whitespaceRE = /\\s+/g;\r\n    // using a non-html document so that <inner/outer>HTML serializes as XML instead\r\n    // of HTML (as we will parse it as xml later)\r\n    const xmlDoc = document.implementation.createDocument(null, null, null);\r\n    const MODS = new Set([\"stop\", \"capture\", \"prevent\", \"self\", \"synthetic\"]);\r\n    let nextDataIds = {};\r\n    function generateId(prefix = \"\") {\r\n        nextDataIds[prefix] = (nextDataIds[prefix] || 0) + 1;\r\n        return prefix + nextDataIds[prefix];\r\n    }\r\n    function isProp(tag, key) {\r\n        switch (tag) {\r\n            case \"input\":\r\n                return (key === \"checked\" ||\r\n                    key === \"indeterminate\" ||\r\n                    key === \"value\" ||\r\n                    key === \"readonly\" ||\r\n                    key === \"readOnly\" ||\r\n                    key === \"disabled\");\r\n            case \"option\":\r\n                return key === \"selected\" || key === \"disabled\";\r\n            case \"textarea\":\r\n                return key === \"value\" || key === \"readonly\" || key === \"readOnly\" || key === \"disabled\";\r\n            case \"select\":\r\n                return key === \"value\" || key === \"disabled\";\r\n            case \"button\":\r\n            case \"optgroup\":\r\n                return key === \"disabled\";\r\n        }\r\n        return false;\r\n    }\r\n    /**\r\n     * Returns a template literal that evaluates to str. You can add interpolation\r\n     * sigils into the string if required\r\n     */\r\n    function toStringExpression(str) {\r\n        return `\\`${str.replace(/\\\\/g, \"\\\\\\\\\").replace(/`/g, \"\\\\`\").replace(/\\$\\{/, \"\\\\${\")}\\``;\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // BlockDescription\r\n    // -----------------------------------------------------------------------------\r\n    class BlockDescription {\r\n        constructor(target, type) {\r\n            this.dynamicTagName = null;\r\n            this.isRoot = false;\r\n            this.hasDynamicChildren = false;\r\n            this.children = [];\r\n            this.data = [];\r\n            this.childNumber = 0;\r\n            this.parentVar = \"\";\r\n            this.id = BlockDescription.nextBlockId++;\r\n            this.varName = \"b\" + this.id;\r\n            this.blockName = \"block\" + this.id;\r\n            this.target = target;\r\n            this.type = type;\r\n        }\r\n        insertData(str, prefix = \"d\") {\r\n            const id = generateId(prefix);\r\n            this.target.addLine(`let ${id} = ${str};`);\r\n            return this.data.push(id) - 1;\r\n        }\r\n        insert(dom) {\r\n            if (this.currentDom) {\r\n                this.currentDom.appendChild(dom);\r\n            }\r\n            else {\r\n                this.dom = dom;\r\n            }\r\n        }\r\n        generateExpr(expr) {\r\n            if (this.type === \"block\") {\r\n                const hasChildren = this.children.length;\r\n                let params = this.data.length ? `[${this.data.join(\", \")}]` : hasChildren ? \"[]\" : \"\";\r\n                if (hasChildren) {\r\n                    params += \", [\" + this.children.map((c) => c.varName).join(\", \") + \"]\";\r\n                }\r\n                if (this.dynamicTagName) {\r\n                    return `toggler(${this.dynamicTagName}, ${this.blockName}(${this.dynamicTagName})(${params}))`;\r\n                }\r\n                return `${this.blockName}(${params})`;\r\n            }\r\n            else if (this.type === \"list\") {\r\n                return `list(c_block${this.id})`;\r\n            }\r\n            return expr;\r\n        }\r\n        asXmlString() {\r\n            // Can't use outerHTML on text/comment nodes\r\n            // append dom to any element and use innerHTML instead\r\n            const t = xmlDoc.createElement(\"t\");\r\n            t.appendChild(this.dom);\r\n            return t.innerHTML;\r\n        }\r\n    }\r\n    BlockDescription.nextBlockId = 1;\r\n    function createContext(parentCtx, params) {\r\n        return Object.assign({\r\n            block: null,\r\n            index: 0,\r\n            forceNewBlock: true,\r\n            translate: parentCtx.translate,\r\n            tKeyExpr: null,\r\n            nameSpace: parentCtx.nameSpace,\r\n            tModelSelectedExpr: parentCtx.tModelSelectedExpr,\r\n        }, params);\r\n    }\r\n    class CodeTarget {\r\n        constructor(name, on) {\r\n            this.indentLevel = 0;\r\n            this.loopLevel = 0;\r\n            this.code = [];\r\n            this.hasRoot = false;\r\n            this.hasCache = false;\r\n            this.shouldProtectScope = false;\r\n            this.hasRefWrapper = false;\r\n            this.name = name;\r\n            this.on = on || null;\r\n        }\r\n        addLine(line, idx) {\r\n            const prefix = new Array(this.indentLevel + 2).join(\"  \");\r\n            if (idx === undefined) {\r\n                this.code.push(prefix + line);\r\n            }\r\n            else {\r\n                this.code.splice(idx, 0, prefix + line);\r\n            }\r\n        }\r\n        generateCode() {\r\n            let result = [];\r\n            result.push(`function ${this.name}(ctx, node, key = \"\") {`);\r\n            if (this.shouldProtectScope) {\r\n                result.push(`  ctx = Object.create(ctx);`);\r\n                result.push(`  ctx[isBoundary] = 1`);\r\n            }\r\n            if (this.hasRefWrapper) {\r\n                result.push(`  let refWrapper = makeRefWrapper(this.__owl__);`);\r\n            }\r\n            if (this.hasCache) {\r\n                result.push(`  let cache = ctx.cache || {};`);\r\n                result.push(`  let nextCache = ctx.cache = {};`);\r\n            }\r\n            for (let line of this.code) {\r\n                result.push(line);\r\n            }\r\n            if (!this.hasRoot) {\r\n                result.push(`return text('');`);\r\n            }\r\n            result.push(`}`);\r\n            return result.join(\"\\n  \");\r\n        }\r\n        currentKey(ctx) {\r\n            let key = this.loopLevel ? `key${this.loopLevel}` : \"key\";\r\n            if (ctx.tKeyExpr) {\r\n                key = `${ctx.tKeyExpr} + ${key}`;\r\n            }\r\n            return key;\r\n        }\r\n    }\r\n    const TRANSLATABLE_ATTRS = [\"label\", \"title\", \"placeholder\", \"alt\"];\r\n    const translationRE = /^(\\s*)([\\s\\S]+?)(\\s*)$/;\r\n    class CodeGenerator {\r\n        constructor(ast, options) {\r\n            this.blocks = [];\r\n            this.nextBlockId = 1;\r\n            this.isDebug = false;\r\n            this.targets = [];\r\n            this.target = new CodeTarget(\"template\");\r\n            this.translatableAttributes = TRANSLATABLE_ATTRS;\r\n            this.staticDefs = [];\r\n            this.slotNames = new Set();\r\n            this.helpers = new Set();\r\n            this.translateFn = options.translateFn || ((s) => s);\r\n            if (options.translatableAttributes) {\r\n                const attrs = new Set(TRANSLATABLE_ATTRS);\r\n                for (let attr of options.translatableAttributes) {\r\n                    if (attr.startsWith(\"-\")) {\r\n                        attrs.delete(attr.slice(1));\r\n                    }\r\n                    else {\r\n                        attrs.add(attr);\r\n                    }\r\n                }\r\n                this.translatableAttributes = [...attrs];\r\n            }\r\n            this.hasSafeContext = options.hasSafeContext || false;\r\n            this.dev = options.dev || false;\r\n            this.ast = ast;\r\n            this.templateName = options.name;\r\n        }\r\n        generateCode() {\r\n            const ast = this.ast;\r\n            this.isDebug = ast.type === 12 /* TDebug */;\r\n            BlockDescription.nextBlockId = 1;\r\n            nextDataIds = {};\r\n            this.compileAST(ast, {\r\n                block: null,\r\n                index: 0,\r\n                forceNewBlock: false,\r\n                isLast: true,\r\n                translate: true,\r\n                tKeyExpr: null,\r\n            });\r\n            // define blocks and utility functions\r\n            let mainCode = [`  let { text, createBlock, list, multi, html, toggler, comment } = bdom;`];\r\n            if (this.helpers.size) {\r\n                mainCode.push(`let { ${[...this.helpers].join(\", \")} } = helpers;`);\r\n            }\r\n            if (this.templateName) {\r\n                mainCode.push(`// Template name: \"${this.templateName}\"`);\r\n            }\r\n            for (let { id, expr } of this.staticDefs) {\r\n                mainCode.push(`const ${id} = ${expr};`);\r\n            }\r\n            // define all blocks\r\n            if (this.blocks.length) {\r\n                mainCode.push(``);\r\n                for (let block of this.blocks) {\r\n                    if (block.dom) {\r\n                        let xmlString = toStringExpression(block.asXmlString());\r\n                        if (block.dynamicTagName) {\r\n                            xmlString = xmlString.replace(/^`<\\w+/, `\\`<\\${tag || '${block.dom.nodeName}'}`);\r\n                            xmlString = xmlString.replace(/\\w+>`$/, `\\${tag || '${block.dom.nodeName}'}>\\``);\r\n                            mainCode.push(`let ${block.blockName} = tag => createBlock(${xmlString});`);\r\n                        }\r\n                        else {\r\n                            mainCode.push(`let ${block.blockName} = createBlock(${xmlString});`);\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n            // define all slots/defaultcontent function\r\n            if (this.targets.length) {\r\n                for (let fn of this.targets) {\r\n                    mainCode.push(\"\");\r\n                    mainCode = mainCode.concat(fn.generateCode());\r\n                }\r\n            }\r\n            // generate main code\r\n            mainCode.push(\"\");\r\n            mainCode = mainCode.concat(\"return \" + this.target.generateCode());\r\n            const code = mainCode.join(\"\\n  \");\r\n            if (this.isDebug) {\r\n                const msg = `[Owl Debug]\\n${code}`;\r\n                console.log(msg);\r\n            }\r\n            return code;\r\n        }\r\n        compileInNewTarget(prefix, ast, ctx, on) {\r\n            const name = generateId(prefix);\r\n            const initialTarget = this.target;\r\n            const target = new CodeTarget(name, on);\r\n            this.targets.push(target);\r\n            this.target = target;\r\n            this.compileAST(ast, createContext(ctx));\r\n            this.target = initialTarget;\r\n            return name;\r\n        }\r\n        addLine(line, idx) {\r\n            this.target.addLine(line, idx);\r\n        }\r\n        define(varName, expr) {\r\n            this.addLine(`const ${varName} = ${expr};`);\r\n        }\r\n        insertAnchor(block, index = block.children.length) {\r\n            const tag = `block-child-${index}`;\r\n            const anchor = xmlDoc.createElement(tag);\r\n            block.insert(anchor);\r\n        }\r\n        createBlock(parentBlock, type, ctx) {\r\n            const hasRoot = this.target.hasRoot;\r\n            const block = new BlockDescription(this.target, type);\r\n            if (!hasRoot) {\r\n                this.target.hasRoot = true;\r\n                block.isRoot = true;\r\n            }\r\n            if (parentBlock) {\r\n                parentBlock.children.push(block);\r\n                if (parentBlock.type === \"list\") {\r\n                    block.parentVar = `c_block${parentBlock.id}`;\r\n                }\r\n            }\r\n            return block;\r\n        }\r\n        insertBlock(expression, block, ctx) {\r\n            let blockExpr = block.generateExpr(expression);\r\n            if (block.parentVar) {\r\n                let key = this.target.currentKey(ctx);\r\n                this.helpers.add(\"withKey\");\r\n                this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${key});`);\r\n                return;\r\n            }\r\n            if (ctx.tKeyExpr) {\r\n                blockExpr = `toggler(${ctx.tKeyExpr}, ${blockExpr})`;\r\n            }\r\n            if (block.isRoot) {\r\n                if (this.target.on) {\r\n                    blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);\r\n                }\r\n                this.addLine(`return ${blockExpr};`);\r\n            }\r\n            else {\r\n                this.define(block.varName, blockExpr);\r\n            }\r\n        }\r\n        /**\r\n         * Captures variables that are used inside of an expression. This is useful\r\n         * because in compiled code, almost all variables are accessed through the ctx\r\n         * object. In the case of functions, that lookup in the context can be delayed\r\n         * which can cause issues if the value has changed since the function was\r\n         * defined.\r\n         *\r\n         * @param expr the expression to capture\r\n         * @param forceCapture whether the expression should capture its scope even if\r\n         *  it doesn't contain a function. Useful when the expression will be used as\r\n         *  a function body.\r\n         * @returns a new expression that uses the captured values\r\n         */\r\n        captureExpression(expr, forceCapture = false) {\r\n            if (!forceCapture && !expr.includes(\"=>\")) {\r\n                return compileExpr(expr);\r\n            }\r\n            const tokens = compileExprToArray(expr);\r\n            const mapping = new Map();\r\n            return tokens\r\n                .map((tok) => {\r\n                if (tok.varName && !tok.isLocal) {\r\n                    if (!mapping.has(tok.varName)) {\r\n                        const varId = generateId(\"v\");\r\n                        mapping.set(tok.varName, varId);\r\n                        this.define(varId, tok.value);\r\n                    }\r\n                    tok.value = mapping.get(tok.varName);\r\n                }\r\n                return tok.value;\r\n            })\r\n                .join(\"\");\r\n        }\r\n        translate(str) {\r\n            const match = translationRE.exec(str);\r\n            return match[1] + this.translateFn(match[2]) + match[3];\r\n        }\r\n        /**\r\n         * @returns the newly created block name, if any\r\n         */\r\n        compileAST(ast, ctx) {\r\n            switch (ast.type) {\r\n                case 1 /* Comment */:\r\n                    return this.compileComment(ast, ctx);\r\n                case 0 /* Text */:\r\n                    return this.compileText(ast, ctx);\r\n                case 2 /* DomNode */:\r\n                    return this.compileTDomNode(ast, ctx);\r\n                case 4 /* TEsc */:\r\n                    return this.compileTEsc(ast, ctx);\r\n                case 8 /* TOut */:\r\n                    return this.compileTOut(ast, ctx);\r\n                case 5 /* TIf */:\r\n                    return this.compileTIf(ast, ctx);\r\n                case 9 /* TForEach */:\r\n                    return this.compileTForeach(ast, ctx);\r\n                case 10 /* TKey */:\r\n                    return this.compileTKey(ast, ctx);\r\n                case 3 /* Multi */:\r\n                    return this.compileMulti(ast, ctx);\r\n                case 7 /* TCall */:\r\n                    return this.compileTCall(ast, ctx);\r\n                case 15 /* TCallBlock */:\r\n                    return this.compileTCallBlock(ast, ctx);\r\n                case 6 /* TSet */:\r\n                    return this.compileTSet(ast, ctx);\r\n                case 11 /* TComponent */:\r\n                    return this.compileComponent(ast, ctx);\r\n                case 12 /* TDebug */:\r\n                    return this.compileDebug(ast, ctx);\r\n                case 13 /* TLog */:\r\n                    return this.compileLog(ast, ctx);\r\n                case 14 /* TSlot */:\r\n                    return this.compileTSlot(ast, ctx);\r\n                case 16 /* TTranslation */:\r\n                    return this.compileTTranslation(ast, ctx);\r\n                case 17 /* TPortal */:\r\n                    return this.compileTPortal(ast, ctx);\r\n            }\r\n        }\r\n        compileDebug(ast, ctx) {\r\n            this.addLine(`debugger;`);\r\n            if (ast.content) {\r\n                return this.compileAST(ast.content, ctx);\r\n            }\r\n            return null;\r\n        }\r\n        compileLog(ast, ctx) {\r\n            this.addLine(`console.log(${compileExpr(ast.expr)});`);\r\n            if (ast.content) {\r\n                return this.compileAST(ast.content, ctx);\r\n            }\r\n            return null;\r\n        }\r\n        compileComment(ast, ctx) {\r\n            let { block, forceNewBlock } = ctx;\r\n            const isNewBlock = !block || forceNewBlock;\r\n            if (isNewBlock) {\r\n                block = this.createBlock(block, \"comment\", ctx);\r\n                this.insertBlock(`comment(${toStringExpression(ast.value)})`, block, {\r\n                    ...ctx,\r\n                    forceNewBlock: forceNewBlock && !block,\r\n                });\r\n            }\r\n            else {\r\n                const text = xmlDoc.createComment(ast.value);\r\n                block.insert(text);\r\n            }\r\n            return block.varName;\r\n        }\r\n        compileText(ast, ctx) {\r\n            let { block, forceNewBlock } = ctx;\r\n            let value = ast.value;\r\n            if (value && ctx.translate !== false) {\r\n                value = this.translate(value);\r\n            }\r\n            if (!ctx.inPreTag) {\r\n                value = value.replace(whitespaceRE, \" \");\r\n            }\r\n            if (!block || forceNewBlock) {\r\n                block = this.createBlock(block, \"text\", ctx);\r\n                this.insertBlock(`text(${toStringExpression(value)})`, block, {\r\n                    ...ctx,\r\n                    forceNewBlock: forceNewBlock && !block,\r\n                });\r\n            }\r\n            else {\r\n                const createFn = ast.type === 0 /* Text */ ? xmlDoc.createTextNode : xmlDoc.createComment;\r\n                block.insert(createFn.call(xmlDoc, value));\r\n            }\r\n            return block.varName;\r\n        }\r\n        generateHandlerCode(rawEvent, handler) {\r\n            const modifiers = rawEvent\r\n                .split(\".\")\r\n                .slice(1)\r\n                .map((m) => {\r\n                if (!MODS.has(m)) {\r\n                    throw new OwlError(`Unknown event modifier: '${m}'`);\r\n                }\r\n                return `\"${m}\"`;\r\n            });\r\n            let modifiersCode = \"\";\r\n            if (modifiers.length) {\r\n                modifiersCode = `${modifiers.join(\",\")}, `;\r\n            }\r\n            return `[${modifiersCode}${this.captureExpression(handler)}, ctx]`;\r\n        }\r\n        compileTDomNode(ast, ctx) {\r\n            let { block, forceNewBlock } = ctx;\r\n            const isNewBlock = !block || forceNewBlock || ast.dynamicTag !== null || ast.ns;\r\n            let codeIdx = this.target.code.length;\r\n            if (isNewBlock) {\r\n                if ((ast.dynamicTag || ctx.tKeyExpr || ast.ns) && ctx.block) {\r\n                    this.insertAnchor(ctx.block);\r\n                }\r\n                block = this.createBlock(block, \"block\", ctx);\r\n                this.blocks.push(block);\r\n                if (ast.dynamicTag) {\r\n                    const tagExpr = generateId(\"tag\");\r\n                    this.define(tagExpr, compileExpr(ast.dynamicTag));\r\n                    block.dynamicTagName = tagExpr;\r\n                }\r\n            }\r\n            // attributes\r\n            const attrs = {};\r\n            for (let key in ast.attrs) {\r\n                let expr, attrName;\r\n                if (key.startsWith(\"t-attf\")) {\r\n                    expr = interpolate(ast.attrs[key]);\r\n                    const idx = block.insertData(expr, \"attr\");\r\n                    attrName = key.slice(7);\r\n                    attrs[\"block-attribute-\" + idx] = attrName;\r\n                }\r\n                else if (key.startsWith(\"t-att\")) {\r\n                    attrName = key === \"t-att\" ? null : key.slice(6);\r\n                    expr = compileExpr(ast.attrs[key]);\r\n                    if (attrName && isProp(ast.tag, attrName)) {\r\n                        if (attrName === \"readonly\") {\r\n                            // the property has a different name than the attribute\r\n                            attrName = \"readOnly\";\r\n                        }\r\n                        // we force a new string or new boolean to bypass the equality check in blockdom when patching same value\r\n                        if (attrName === \"value\") {\r\n                            // When the expression is falsy (except 0), fall back to an empty string\r\n                            expr = `new String((${expr}) === 0 ? 0 : ((${expr}) || \"\"))`;\r\n                        }\r\n                        else {\r\n                            expr = `new Boolean(${expr})`;\r\n                        }\r\n                        const idx = block.insertData(expr, \"prop\");\r\n                        attrs[`block-property-${idx}`] = attrName;\r\n                    }\r\n                    else {\r\n                        const idx = block.insertData(expr, \"attr\");\r\n                        if (key === \"t-att\") {\r\n                            attrs[`block-attributes`] = String(idx);\r\n                        }\r\n                        else {\r\n                            attrs[`block-attribute-${idx}`] = attrName;\r\n                        }\r\n                    }\r\n                }\r\n                else if (this.translatableAttributes.includes(key)) {\r\n                    attrs[key] = this.translateFn(ast.attrs[key]);\r\n                }\r\n                else {\r\n                    expr = `\"${ast.attrs[key]}\"`;\r\n                    attrName = key;\r\n                    attrs[key] = ast.attrs[key];\r\n                }\r\n                if (attrName === \"value\" && ctx.tModelSelectedExpr) {\r\n                    let selectedId = block.insertData(`${ctx.tModelSelectedExpr} === ${expr}`, \"attr\");\r\n                    attrs[`block-attribute-${selectedId}`] = \"selected\";\r\n                }\r\n            }\r\n            // t-model\r\n            let tModelSelectedExpr;\r\n            if (ast.model) {\r\n                const { hasDynamicChildren, baseExpr, expr, eventType, shouldNumberize, shouldTrim, targetAttr, specialInitTargetAttr, } = ast.model;\r\n                const baseExpression = compileExpr(baseExpr);\r\n                const bExprId = generateId(\"bExpr\");\r\n                this.define(bExprId, baseExpression);\r\n                const expression = compileExpr(expr);\r\n                const exprId = generateId(\"expr\");\r\n                this.define(exprId, expression);\r\n                const fullExpression = `${bExprId}[${exprId}]`;\r\n                let idx;\r\n                if (specialInitTargetAttr) {\r\n                    let targetExpr = targetAttr in attrs && `'${attrs[targetAttr]}'`;\r\n                    if (!targetExpr && ast.attrs) {\r\n                        // look at the dynamic attribute counterpart\r\n                        const dynamicTgExpr = ast.attrs[`t-att-${targetAttr}`];\r\n                        if (dynamicTgExpr) {\r\n                            targetExpr = compileExpr(dynamicTgExpr);\r\n                        }\r\n                    }\r\n                    idx = block.insertData(`${fullExpression} === ${targetExpr}`, \"prop\");\r\n                    attrs[`block-property-${idx}`] = specialInitTargetAttr;\r\n                }\r\n                else if (hasDynamicChildren) {\r\n                    const bValueId = generateId(\"bValue\");\r\n                    tModelSelectedExpr = `${bValueId}`;\r\n                    this.define(tModelSelectedExpr, fullExpression);\r\n                }\r\n                else {\r\n                    idx = block.insertData(`${fullExpression}`, \"prop\");\r\n                    attrs[`block-property-${idx}`] = targetAttr;\r\n                }\r\n                this.helpers.add(\"toNumber\");\r\n                let valueCode = `ev.target.${targetAttr}`;\r\n                valueCode = shouldTrim ? `${valueCode}.trim()` : valueCode;\r\n                valueCode = shouldNumberize ? `toNumber(${valueCode})` : valueCode;\r\n                const handler = `[(ev) => { ${fullExpression} = ${valueCode}; }]`;\r\n                idx = block.insertData(handler, \"hdlr\");\r\n                attrs[`block-handler-${idx}`] = eventType;\r\n            }\r\n            // event handlers\r\n            for (let ev in ast.on) {\r\n                const name = this.generateHandlerCode(ev, ast.on[ev]);\r\n                const idx = block.insertData(name, \"hdlr\");\r\n                attrs[`block-handler-${idx}`] = ev;\r\n            }\r\n            // t-ref\r\n            if (ast.ref) {\r\n                if (this.dev) {\r\n                    this.helpers.add(\"makeRefWrapper\");\r\n                    this.target.hasRefWrapper = true;\r\n                }\r\n                const isDynamic = INTERP_REGEXP.test(ast.ref);\r\n                let name = `\\`${ast.ref}\\``;\r\n                if (isDynamic) {\r\n                    name = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));\r\n                }\r\n                let setRefStr = `(el) => this.__owl__.setRef((${name}), el)`;\r\n                if (this.dev) {\r\n                    setRefStr = `refWrapper(${name}, ${setRefStr})`;\r\n                }\r\n                const idx = block.insertData(setRefStr, \"ref\");\r\n                attrs[\"block-ref\"] = String(idx);\r\n            }\r\n            const nameSpace = ast.ns || ctx.nameSpace;\r\n            const dom = nameSpace\r\n                ? xmlDoc.createElementNS(nameSpace, ast.tag)\r\n                : xmlDoc.createElement(ast.tag);\r\n            for (const [attr, val] of Object.entries(attrs)) {\r\n                if (!(attr === \"class\" && val === \"\")) {\r\n                    dom.setAttribute(attr, val);\r\n                }\r\n            }\r\n            block.insert(dom);\r\n            if (ast.content.length) {\r\n                const initialDom = block.currentDom;\r\n                block.currentDom = dom;\r\n                const children = ast.content;\r\n                for (let i = 0; i < children.length; i++) {\r\n                    const child = ast.content[i];\r\n                    const subCtx = createContext(ctx, {\r\n                        block,\r\n                        index: block.childNumber,\r\n                        forceNewBlock: false,\r\n                        isLast: ctx.isLast && i === children.length - 1,\r\n                        tKeyExpr: ctx.tKeyExpr,\r\n                        nameSpace,\r\n                        tModelSelectedExpr,\r\n                        inPreTag: ctx.inPreTag || ast.tag === \"pre\",\r\n                    });\r\n                    this.compileAST(child, subCtx);\r\n                }\r\n                block.currentDom = initialDom;\r\n            }\r\n            if (isNewBlock) {\r\n                this.insertBlock(`${block.blockName}(ddd)`, block, ctx);\r\n                // may need to rewrite code!\r\n                if (block.children.length && block.hasDynamicChildren) {\r\n                    const code = this.target.code;\r\n                    const children = block.children.slice();\r\n                    let current = children.shift();\r\n                    for (let i = codeIdx; i < code.length; i++) {\r\n                        if (code[i].trimStart().startsWith(`const ${current.varName} `)) {\r\n                            code[i] = code[i].replace(`const ${current.varName}`, current.varName);\r\n                            current = children.shift();\r\n                            if (!current)\r\n                                break;\r\n                        }\r\n                    }\r\n                    this.addLine(`let ${block.children.map((c) => c.varName).join(\", \")};`, codeIdx);\r\n                }\r\n            }\r\n            return block.varName;\r\n        }\r\n        compileTEsc(ast, ctx) {\r\n            let { block, forceNewBlock } = ctx;\r\n            let expr;\r\n            if (ast.expr === \"0\") {\r\n                this.helpers.add(\"zero\");\r\n                expr = `ctx[zero]`;\r\n            }\r\n            else {\r\n                expr = compileExpr(ast.expr);\r\n                if (ast.defaultValue) {\r\n                    this.helpers.add(\"withDefault\");\r\n                    // FIXME: defaultValue is not translated\r\n                    expr = `withDefault(${expr}, ${toStringExpression(ast.defaultValue)})`;\r\n                }\r\n            }\r\n            if (!block || forceNewBlock) {\r\n                block = this.createBlock(block, \"text\", ctx);\r\n                this.insertBlock(`text(${expr})`, block, { ...ctx, forceNewBlock: forceNewBlock && !block });\r\n            }\r\n            else {\r\n                const idx = block.insertData(expr, \"txt\");\r\n                const text = xmlDoc.createElement(`block-text-${idx}`);\r\n                block.insert(text);\r\n            }\r\n            return block.varName;\r\n        }\r\n        compileTOut(ast, ctx) {\r\n            let { block } = ctx;\r\n            if (block) {\r\n                this.insertAnchor(block);\r\n            }\r\n            block = this.createBlock(block, \"html\", ctx);\r\n            let blockStr;\r\n            if (ast.expr === \"0\") {\r\n                this.helpers.add(\"zero\");\r\n                blockStr = `ctx[zero]`;\r\n            }\r\n            else if (ast.body) {\r\n                let bodyValue = null;\r\n                bodyValue = BlockDescription.nextBlockId;\r\n                const subCtx = createContext(ctx);\r\n                this.compileAST({ type: 3 /* Multi */, content: ast.body }, subCtx);\r\n                this.helpers.add(\"safeOutput\");\r\n                blockStr = `safeOutput(${compileExpr(ast.expr)}, b${bodyValue})`;\r\n            }\r\n            else {\r\n                this.helpers.add(\"safeOutput\");\r\n                blockStr = `safeOutput(${compileExpr(ast.expr)})`;\r\n            }\r\n            this.insertBlock(blockStr, block, ctx);\r\n            return block.varName;\r\n        }\r\n        compileTIfBranch(content, block, ctx) {\r\n            this.target.indentLevel++;\r\n            let childN = block.children.length;\r\n            this.compileAST(content, createContext(ctx, { block, index: ctx.index }));\r\n            if (block.children.length > childN) {\r\n                // we have some content => need to insert an anchor at correct index\r\n                this.insertAnchor(block, childN);\r\n            }\r\n            this.target.indentLevel--;\r\n        }\r\n        compileTIf(ast, ctx, nextNode) {\r\n            let { block, forceNewBlock } = ctx;\r\n            const codeIdx = this.target.code.length;\r\n            const isNewBlock = !block || (block.type !== \"multi\" && forceNewBlock);\r\n            if (block) {\r\n                block.hasDynamicChildren = true;\r\n            }\r\n            if (!block || (block.type !== \"multi\" && forceNewBlock)) {\r\n                block = this.createBlock(block, \"multi\", ctx);\r\n            }\r\n            this.addLine(`if (${compileExpr(ast.condition)}) {`);\r\n            this.compileTIfBranch(ast.content, block, ctx);\r\n            if (ast.tElif) {\r\n                for (let clause of ast.tElif) {\r\n                    this.addLine(`} else if (${compileExpr(clause.condition)}) {`);\r\n                    this.compileTIfBranch(clause.content, block, ctx);\r\n                }\r\n            }\r\n            if (ast.tElse) {\r\n                this.addLine(`} else {`);\r\n                this.compileTIfBranch(ast.tElse, block, ctx);\r\n            }\r\n            this.addLine(\"}\");\r\n            if (isNewBlock) {\r\n                // note: this part is duplicated from end of compiledomnode:\r\n                if (block.children.length) {\r\n                    const code = this.target.code;\r\n                    const children = block.children.slice();\r\n                    let current = children.shift();\r\n                    for (let i = codeIdx; i < code.length; i++) {\r\n                        if (code[i].trimStart().startsWith(`const ${current.varName} `)) {\r\n                            code[i] = code[i].replace(`const ${current.varName}`, current.varName);\r\n                            current = children.shift();\r\n                            if (!current)\r\n                                break;\r\n                        }\r\n                    }\r\n                    this.addLine(`let ${block.children.map((c) => c.varName).join(\", \")};`, codeIdx);\r\n                }\r\n                // note: this part is duplicated from end of compilemulti:\r\n                const args = block.children.map((c) => c.varName).join(\", \");\r\n                this.insertBlock(`multi([${args}])`, block, ctx);\r\n            }\r\n            return block.varName;\r\n        }\r\n        compileTForeach(ast, ctx) {\r\n            let { block } = ctx;\r\n            if (block) {\r\n                this.insertAnchor(block);\r\n            }\r\n            block = this.createBlock(block, \"list\", ctx);\r\n            this.target.loopLevel++;\r\n            const loopVar = `i${this.target.loopLevel}`;\r\n            this.addLine(`ctx = Object.create(ctx);`);\r\n            const vals = `v_block${block.id}`;\r\n            const keys = `k_block${block.id}`;\r\n            const l = `l_block${block.id}`;\r\n            const c = `c_block${block.id}`;\r\n            this.helpers.add(\"prepareList\");\r\n            this.define(`[${keys}, ${vals}, ${l}, ${c}]`, `prepareList(${compileExpr(ast.collection)});`);\r\n            // Throw errors on duplicate keys in dev mode\r\n            if (this.dev) {\r\n                this.define(`keys${block.id}`, `new Set()`);\r\n            }\r\n            this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`);\r\n            this.target.indentLevel++;\r\n            this.addLine(`ctx[\\`${ast.elem}\\`] = ${keys}[${loopVar}];`);\r\n            if (!ast.hasNoFirst) {\r\n                this.addLine(`ctx[\\`${ast.elem}_first\\`] = ${loopVar} === 0;`);\r\n            }\r\n            if (!ast.hasNoLast) {\r\n                this.addLine(`ctx[\\`${ast.elem}_last\\`] = ${loopVar} === ${keys}.length - 1;`);\r\n            }\r\n            if (!ast.hasNoIndex) {\r\n                this.addLine(`ctx[\\`${ast.elem}_index\\`] = ${loopVar};`);\r\n            }\r\n            if (!ast.hasNoValue) {\r\n                this.addLine(`ctx[\\`${ast.elem}_value\\`] = ${vals}[${loopVar}];`);\r\n            }\r\n            this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);\r\n            if (this.dev) {\r\n                // Throw error on duplicate keys in dev mode\r\n                this.helpers.add(\"OwlError\");\r\n                this.addLine(`if (keys${block.id}.has(String(key${this.target.loopLevel}))) { throw new OwlError(\\`Got duplicate key in t-foreach: \\${key${this.target.loopLevel}}\\`)}`);\r\n                this.addLine(`keys${block.id}.add(String(key${this.target.loopLevel}));`);\r\n            }\r\n            let id;\r\n            if (ast.memo) {\r\n                this.target.hasCache = true;\r\n                id = generateId();\r\n                this.define(`memo${id}`, compileExpr(ast.memo));\r\n                this.define(`vnode${id}`, `cache[key${this.target.loopLevel}];`);\r\n                this.addLine(`if (vnode${id}) {`);\r\n                this.target.indentLevel++;\r\n                this.addLine(`if (shallowEqual(vnode${id}.memo, memo${id})) {`);\r\n                this.target.indentLevel++;\r\n                this.addLine(`${c}[${loopVar}] = vnode${id};`);\r\n                this.addLine(`nextCache[key${this.target.loopLevel}] = vnode${id};`);\r\n                this.addLine(`continue;`);\r\n                this.target.indentLevel--;\r\n                this.addLine(\"}\");\r\n                this.target.indentLevel--;\r\n                this.addLine(\"}\");\r\n            }\r\n            const subCtx = createContext(ctx, { block, index: loopVar });\r\n            this.compileAST(ast.body, subCtx);\r\n            if (ast.memo) {\r\n                this.addLine(`nextCache[key${this.target.loopLevel}] = Object.assign(${c}[${loopVar}], {memo: memo${id}});`);\r\n            }\r\n            this.target.indentLevel--;\r\n            this.target.loopLevel--;\r\n            this.addLine(`}`);\r\n            if (!ctx.isLast) {\r\n                this.addLine(`ctx = ctx.__proto__;`);\r\n            }\r\n            this.insertBlock(\"l\", block, ctx);\r\n            return block.varName;\r\n        }\r\n        compileTKey(ast, ctx) {\r\n            const tKeyExpr = generateId(\"tKey_\");\r\n            this.define(tKeyExpr, compileExpr(ast.expr));\r\n            ctx = createContext(ctx, {\r\n                tKeyExpr,\r\n                block: ctx.block,\r\n                index: ctx.index,\r\n            });\r\n            return this.compileAST(ast.content, ctx);\r\n        }\r\n        compileMulti(ast, ctx) {\r\n            let { block, forceNewBlock } = ctx;\r\n            const isNewBlock = !block || forceNewBlock;\r\n            let codeIdx = this.target.code.length;\r\n            if (isNewBlock) {\r\n                const n = ast.content.filter((c) => c.type !== 6 /* TSet */).length;\r\n                let result = null;\r\n                if (n <= 1) {\r\n                    for (let child of ast.content) {\r\n                        const blockName = this.compileAST(child, ctx);\r\n                        result = result || blockName;\r\n                    }\r\n                    return result;\r\n                }\r\n                block = this.createBlock(block, \"multi\", ctx);\r\n            }\r\n            let index = 0;\r\n            for (let i = 0, l = ast.content.length; i < l; i++) {\r\n                const child = ast.content[i];\r\n                const isTSet = child.type === 6 /* TSet */;\r\n                const subCtx = createContext(ctx, {\r\n                    block,\r\n                    index,\r\n                    forceNewBlock: !isTSet,\r\n                    isLast: ctx.isLast && i === l - 1,\r\n                });\r\n                this.compileAST(child, subCtx);\r\n                if (!isTSet) {\r\n                    index++;\r\n                }\r\n            }\r\n            if (isNewBlock) {\r\n                if (block.hasDynamicChildren && block.children.length) {\r\n                    const code = this.target.code;\r\n                    const children = block.children.slice();\r\n                    let current = children.shift();\r\n                    for (let i = codeIdx; i < code.length; i++) {\r\n                        if (code[i].trimStart().startsWith(`const ${current.varName} `)) {\r\n                            code[i] = code[i].replace(`const ${current.varName}`, current.varName);\r\n                            current = children.shift();\r\n                            if (!current)\r\n                                break;\r\n                        }\r\n                    }\r\n                    this.addLine(`let ${block.children.map((c) => c.varName).join(\", \")};`, codeIdx);\r\n                }\r\n                const args = block.children.map((c) => c.varName).join(\", \");\r\n                this.insertBlock(`multi([${args}])`, block, ctx);\r\n            }\r\n            return block.varName;\r\n        }\r\n        compileTCall(ast, ctx) {\r\n            let { block, forceNewBlock } = ctx;\r\n            let ctxVar = ctx.ctxVar || \"ctx\";\r\n            if (ast.context) {\r\n                ctxVar = generateId(\"ctx\");\r\n                this.addLine(`let ${ctxVar} = ${compileExpr(ast.context)};`);\r\n            }\r\n            const isDynamic = INTERP_REGEXP.test(ast.name);\r\n            const subTemplate = isDynamic ? interpolate(ast.name) : \"`\" + ast.name + \"`\";\r\n            if (block && !forceNewBlock) {\r\n                this.insertAnchor(block);\r\n            }\r\n            block = this.createBlock(block, \"multi\", ctx);\r\n            if (ast.body) {\r\n                this.addLine(`${ctxVar} = Object.create(${ctxVar});`);\r\n                this.addLine(`${ctxVar}[isBoundary] = 1;`);\r\n                this.helpers.add(\"isBoundary\");\r\n                const subCtx = createContext(ctx, { ctxVar });\r\n                const bl = this.compileMulti({ type: 3 /* Multi */, content: ast.body }, subCtx);\r\n                if (bl) {\r\n                    this.helpers.add(\"zero\");\r\n                    this.addLine(`${ctxVar}[zero] = ${bl};`);\r\n                }\r\n            }\r\n            const key = this.generateComponentKey();\r\n            if (isDynamic) {\r\n                const templateVar = generateId(\"template\");\r\n                if (!this.staticDefs.find((d) => d.id === \"call\")) {\r\n                    this.staticDefs.push({ id: \"call\", expr: `app.callTemplate.bind(app)` });\r\n                }\r\n                this.define(templateVar, subTemplate);\r\n                this.insertBlock(`call(this, ${templateVar}, ${ctxVar}, node, ${key})`, block, {\r\n                    ...ctx,\r\n                    forceNewBlock: !block,\r\n                });\r\n            }\r\n            else {\r\n                const id = generateId(`callTemplate_`);\r\n                this.staticDefs.push({ id, expr: `app.getTemplate(${subTemplate})` });\r\n                this.insertBlock(`${id}.call(this, ${ctxVar}, node, ${key})`, block, {\r\n                    ...ctx,\r\n                    forceNewBlock: !block,\r\n                });\r\n            }\r\n            if (ast.body && !ctx.isLast) {\r\n                this.addLine(`${ctxVar} = ${ctxVar}.__proto__;`);\r\n            }\r\n            return block.varName;\r\n        }\r\n        compileTCallBlock(ast, ctx) {\r\n            let { block, forceNewBlock } = ctx;\r\n            if (block) {\r\n                if (!forceNewBlock) {\r\n                    this.insertAnchor(block);\r\n                }\r\n            }\r\n            block = this.createBlock(block, \"multi\", ctx);\r\n            this.insertBlock(compileExpr(ast.name), block, { ...ctx, forceNewBlock: !block });\r\n            return block.varName;\r\n        }\r\n        compileTSet(ast, ctx) {\r\n            this.target.shouldProtectScope = true;\r\n            this.helpers.add(\"isBoundary\").add(\"withDefault\");\r\n            const expr = ast.value ? compileExpr(ast.value || \"\") : \"null\";\r\n            if (ast.body) {\r\n                this.helpers.add(\"LazyValue\");\r\n                const bodyAst = { type: 3 /* Multi */, content: ast.body };\r\n                const name = this.compileInNewTarget(\"value\", bodyAst, ctx);\r\n                let key = this.target.currentKey(ctx);\r\n                let value = `new LazyValue(${name}, ctx, this, node, ${key})`;\r\n                value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value;\r\n                this.addLine(`ctx[\\`${ast.name}\\`] = ${value};`);\r\n            }\r\n            else {\r\n                let value;\r\n                if (ast.defaultValue) {\r\n                    const defaultValue = toStringExpression(ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue);\r\n                    if (ast.value) {\r\n                        value = `withDefault(${expr}, ${defaultValue})`;\r\n                    }\r\n                    else {\r\n                        value = defaultValue;\r\n                    }\r\n                }\r\n                else {\r\n                    value = expr;\r\n                }\r\n                this.helpers.add(\"setContextValue\");\r\n                this.addLine(`setContextValue(${ctx.ctxVar || \"ctx\"}, \"${ast.name}\", ${value});`);\r\n            }\r\n            return null;\r\n        }\r\n        generateComponentKey(currentKey = \"key\") {\r\n            const parts = [generateId(\"__\")];\r\n            for (let i = 0; i < this.target.loopLevel; i++) {\r\n                parts.push(`\\${key${i + 1}}`);\r\n            }\r\n            return `${currentKey} + \\`${parts.join(\"__\")}\\``;\r\n        }\r\n        /**\r\n         * Formats a prop name and value into a string suitable to be inserted in the\r\n         * generated code. For example:\r\n         *\r\n         * Name              Value            Result\r\n         * ---------------------------------------------------------\r\n         * \"number\"          \"state\"          \"number: ctx['state']\"\r\n         * \"something\"       \"\"               \"something: undefined\"\r\n         * \"some-prop\"       \"state\"          \"'some-prop': ctx['state']\"\r\n         * \"onClick.bind\"    \"onClick\"        \"onClick: bind(ctx, ctx['onClick'])\"\r\n         */\r\n        formatProp(name, value) {\r\n            if (name.endsWith(\".translate\")) {\r\n                value = toStringExpression(this.translateFn(value));\r\n            }\r\n            else {\r\n                value = this.captureExpression(value);\r\n            }\r\n            if (name.includes(\".\")) {\r\n                let [_name, suffix] = name.split(\".\");\r\n                name = _name;\r\n                switch (suffix) {\r\n                    case \"bind\":\r\n                        value = `(${value}).bind(this)`;\r\n                        break;\r\n                    case \"alike\":\r\n                    case \"translate\":\r\n                        break;\r\n                    default:\r\n                        throw new OwlError(\"Invalid prop suffix\");\r\n                }\r\n            }\r\n            name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;\r\n            return `${name}: ${value || undefined}`;\r\n        }\r\n        formatPropObject(obj) {\r\n            return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));\r\n        }\r\n        getPropString(props, dynProps) {\r\n            let propString = `{${props.join(\",\")}}`;\r\n            if (dynProps) {\r\n                propString = `Object.assign({}, ${compileExpr(dynProps)}${props.length ? \", \" + propString : \"\"})`;\r\n            }\r\n            return propString;\r\n        }\r\n        compileComponent(ast, ctx) {\r\n            let { block } = ctx;\r\n            // props\r\n            const hasSlotsProp = \"slots\" in (ast.props || {});\r\n            const props = ast.props ? this.formatPropObject(ast.props) : [];\r\n            // slots\r\n            let slotDef = \"\";\r\n            if (ast.slots) {\r\n                let ctxStr = \"ctx\";\r\n                if (this.target.loopLevel || !this.hasSafeContext) {\r\n                    ctxStr = generateId(\"ctx\");\r\n                    this.helpers.add(\"capture\");\r\n                    this.define(ctxStr, `capture(ctx)`);\r\n                }\r\n                let slotStr = [];\r\n                for (let slotName in ast.slots) {\r\n                    const slotAst = ast.slots[slotName];\r\n                    const params = [];\r\n                    if (slotAst.content) {\r\n                        const name = this.compileInNewTarget(\"slot\", slotAst.content, ctx, slotAst.on);\r\n                        params.push(`__render: ${name}.bind(this), __ctx: ${ctxStr}`);\r\n                    }\r\n                    const scope = ast.slots[slotName].scope;\r\n                    if (scope) {\r\n                        params.push(`__scope: \"${scope}\"`);\r\n                    }\r\n                    if (ast.slots[slotName].attrs) {\r\n                        params.push(...this.formatPropObject(ast.slots[slotName].attrs));\r\n                    }\r\n                    const slotInfo = `{${params.join(\", \")}}`;\r\n                    slotStr.push(`'${slotName}': ${slotInfo}`);\r\n                }\r\n                slotDef = `{${slotStr.join(\", \")}}`;\r\n            }\r\n            if (slotDef && !(ast.dynamicProps || hasSlotsProp)) {\r\n                this.helpers.add(\"markRaw\");\r\n                props.push(`slots: markRaw(${slotDef})`);\r\n            }\r\n            let propString = this.getPropString(props, ast.dynamicProps);\r\n            let propVar;\r\n            if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {\r\n                propVar = generateId(\"props\");\r\n                this.define(propVar, propString);\r\n                propString = propVar;\r\n            }\r\n            if (slotDef && (ast.dynamicProps || hasSlotsProp)) {\r\n                this.helpers.add(\"markRaw\");\r\n                this.addLine(`${propVar}.slots = markRaw(Object.assign(${slotDef}, ${propVar}.slots))`);\r\n            }\r\n            // cmap key\r\n            let expr;\r\n            if (ast.isDynamic) {\r\n                expr = generateId(\"Comp\");\r\n                this.define(expr, compileExpr(ast.name));\r\n            }\r\n            else {\r\n                expr = `\\`${ast.name}\\``;\r\n            }\r\n            if (this.dev) {\r\n                this.addLine(`helpers.validateProps(${expr}, ${propVar}, this);`);\r\n            }\r\n            if (block && (ctx.forceNewBlock === false || ctx.tKeyExpr)) {\r\n                // todo: check the forcenewblock condition\r\n                this.insertAnchor(block);\r\n            }\r\n            let keyArg = this.generateComponentKey();\r\n            if (ctx.tKeyExpr) {\r\n                keyArg = `${ctx.tKeyExpr} + ${keyArg}`;\r\n            }\r\n            let id = generateId(\"comp\");\r\n            const propList = [];\r\n            for (let p in ast.props || {}) {\r\n                let [name, suffix] = p.split(\".\");\r\n                if (!suffix) {\r\n                    propList.push(`\"${name}\"`);\r\n                }\r\n            }\r\n            this.staticDefs.push({\r\n                id,\r\n                expr: `app.createComponent(${ast.isDynamic ? null : expr}, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, [${propList}])`,\r\n            });\r\n            if (ast.isDynamic) {\r\n                // If the component class changes, this can cause delayed renders to go\r\n                // through if the key doesn't change. Use the component name for now.\r\n                // This means that two component classes with the same name isn't supported\r\n                // in t-component. We can generate a unique id per class later if needed.\r\n                keyArg = `(${expr}).name + ${keyArg}`;\r\n            }\r\n            let blockExpr = `${id}(${propString}, ${keyArg}, node, this, ${ast.isDynamic ? expr : null})`;\r\n            if (ast.isDynamic) {\r\n                blockExpr = `toggler(${expr}, ${blockExpr})`;\r\n            }\r\n            // event handling\r\n            if (ast.on) {\r\n                blockExpr = this.wrapWithEventCatcher(blockExpr, ast.on);\r\n            }\r\n            block = this.createBlock(block, \"multi\", ctx);\r\n            this.insertBlock(blockExpr, block, ctx);\r\n            return block.varName;\r\n        }\r\n        wrapWithEventCatcher(expr, on) {\r\n            this.helpers.add(\"createCatcher\");\r\n            let name = generateId(\"catcher\");\r\n            let spec = {};\r\n            let handlers = [];\r\n            for (let ev in on) {\r\n                let handlerId = generateId(\"hdlr\");\r\n                let idx = handlers.push(handlerId) - 1;\r\n                spec[ev] = idx;\r\n                const handler = this.generateHandlerCode(ev, on[ev]);\r\n                this.define(handlerId, handler);\r\n            }\r\n            this.staticDefs.push({ id: name, expr: `createCatcher(${JSON.stringify(spec)})` });\r\n            return `${name}(${expr}, [${handlers.join(\",\")}])`;\r\n        }\r\n        compileTSlot(ast, ctx) {\r\n            this.helpers.add(\"callSlot\");\r\n            let { block } = ctx;\r\n            let blockString;\r\n            let slotName;\r\n            let dynamic = false;\r\n            let isMultiple = false;\r\n            if (ast.name.match(INTERP_REGEXP)) {\r\n                dynamic = true;\r\n                isMultiple = true;\r\n                slotName = interpolate(ast.name);\r\n            }\r\n            else {\r\n                slotName = \"'\" + ast.name + \"'\";\r\n                isMultiple = isMultiple || this.slotNames.has(ast.name);\r\n                this.slotNames.add(ast.name);\r\n            }\r\n            const dynProps = ast.attrs ? ast.attrs[\"t-props\"] : null;\r\n            if (ast.attrs) {\r\n                delete ast.attrs[\"t-props\"];\r\n            }\r\n            let key = this.target.loopLevel ? `key${this.target.loopLevel}` : \"key\";\r\n            if (isMultiple) {\r\n                key = this.generateComponentKey(key);\r\n            }\r\n            const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];\r\n            const scope = this.getPropString(props, dynProps);\r\n            if (ast.defaultContent) {\r\n                const name = this.compileInNewTarget(\"defaultContent\", ast.defaultContent, ctx);\r\n                blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope}, ${name}.bind(this))`;\r\n            }\r\n            else {\r\n                if (dynamic) {\r\n                    let name = generateId(\"slot\");\r\n                    this.define(name, slotName);\r\n                    blockString = `toggler(${name}, callSlot(ctx, node, ${key}, ${name}, ${dynamic}, ${scope}))`;\r\n                }\r\n                else {\r\n                    blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope})`;\r\n                }\r\n            }\r\n            // event handling\r\n            if (ast.on) {\r\n                blockString = this.wrapWithEventCatcher(blockString, ast.on);\r\n            }\r\n            if (block) {\r\n                this.insertAnchor(block);\r\n            }\r\n            block = this.createBlock(block, \"multi\", ctx);\r\n            this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });\r\n            return block.varName;\r\n        }\r\n        compileTTranslation(ast, ctx) {\r\n            if (ast.content) {\r\n                return this.compileAST(ast.content, Object.assign({}, ctx, { translate: false }));\r\n            }\r\n            return null;\r\n        }\r\n        compileTPortal(ast, ctx) {\r\n            if (!this.staticDefs.find((d) => d.id === \"Portal\")) {\r\n                this.staticDefs.push({ id: \"Portal\", expr: `app.Portal` });\r\n            }\r\n            let { block } = ctx;\r\n            const name = this.compileInNewTarget(\"slot\", ast.content, ctx);\r\n            let ctxStr = \"ctx\";\r\n            if (this.target.loopLevel || !this.hasSafeContext) {\r\n                ctxStr = generateId(\"ctx\");\r\n                this.helpers.add(\"capture\");\r\n                this.define(ctxStr, `capture(ctx)`);\r\n            }\r\n            let id = generateId(\"comp\");\r\n            this.staticDefs.push({\r\n                id,\r\n                expr: `app.createComponent(null, false, true, false, false)`,\r\n            });\r\n            const target = compileExpr(ast.target);\r\n            const key = this.generateComponentKey();\r\n            const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, ${key}, node, ctx, Portal)`;\r\n            if (block) {\r\n                this.insertAnchor(block);\r\n            }\r\n            block = this.createBlock(block, \"multi\", ctx);\r\n            this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });\r\n            return block.varName;\r\n        }\r\n    }\r\n\r\n    // -----------------------------------------------------------------------------\r\n    // Parser\r\n    // -----------------------------------------------------------------------------\r\n    const cache = new WeakMap();\r\n    function parse(xml) {\r\n        if (typeof xml === \"string\") {\r\n            const elem = parseXML(`<t>${xml}</t>`).firstChild;\r\n            return _parse(elem);\r\n        }\r\n        let ast = cache.get(xml);\r\n        if (!ast) {\r\n            // we clone here the xml to prevent modifying it in place\r\n            ast = _parse(xml.cloneNode(true));\r\n            cache.set(xml, ast);\r\n        }\r\n        return ast;\r\n    }\r\n    function _parse(xml) {\r\n        normalizeXML(xml);\r\n        const ctx = { inPreTag: false };\r\n        return parseNode(xml, ctx) || { type: 0 /* Text */, value: \"\" };\r\n    }\r\n    function parseNode(node, ctx) {\r\n        if (!(node instanceof Element)) {\r\n            return parseTextCommentNode(node, ctx);\r\n        }\r\n        return (parseTDebugLog(node, ctx) ||\r\n            parseTForEach(node, ctx) ||\r\n            parseTIf(node, ctx) ||\r\n            parseTPortal(node, ctx) ||\r\n            parseTCall(node, ctx) ||\r\n            parseTCallBlock(node) ||\r\n            parseTEscNode(node, ctx) ||\r\n            parseTOutNode(node, ctx) ||\r\n            parseTKey(node, ctx) ||\r\n            parseTTranslation(node, ctx) ||\r\n            parseTSlot(node, ctx) ||\r\n            parseComponent(node, ctx) ||\r\n            parseDOMNode(node, ctx) ||\r\n            parseTSetNode(node, ctx) ||\r\n            parseTNode(node, ctx));\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // <t /> tag\r\n    // -----------------------------------------------------------------------------\r\n    function parseTNode(node, ctx) {\r\n        if (node.tagName !== \"t\") {\r\n            return null;\r\n        }\r\n        return parseChildNodes(node, ctx);\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Text and Comment Nodes\r\n    // -----------------------------------------------------------------------------\r\n    const lineBreakRE = /[\\r\\n]/;\r\n    function parseTextCommentNode(node, ctx) {\r\n        if (node.nodeType === Node.TEXT_NODE) {\r\n            let value = node.textContent || \"\";\r\n            if (!ctx.inPreTag && lineBreakRE.test(value) && !value.trim()) {\r\n                return null;\r\n            }\r\n            return { type: 0 /* Text */, value };\r\n        }\r\n        else if (node.nodeType === Node.COMMENT_NODE) {\r\n            return { type: 1 /* Comment */, value: node.textContent || \"\" };\r\n        }\r\n        return null;\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // debugging\r\n    // -----------------------------------------------------------------------------\r\n    function parseTDebugLog(node, ctx) {\r\n        if (node.hasAttribute(\"t-debug\")) {\r\n            node.removeAttribute(\"t-debug\");\r\n            return {\r\n                type: 12 /* TDebug */,\r\n                content: parseNode(node, ctx),\r\n            };\r\n        }\r\n        if (node.hasAttribute(\"t-log\")) {\r\n            const expr = node.getAttribute(\"t-log\");\r\n            node.removeAttribute(\"t-log\");\r\n            return {\r\n                type: 13 /* TLog */,\r\n                expr,\r\n                content: parseNode(node, ctx),\r\n            };\r\n        }\r\n        return null;\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Regular dom node\r\n    // -----------------------------------------------------------------------------\r\n    const hasDotAtTheEnd = /\\.[\\w_]+\\s*$/;\r\n    const hasBracketsAtTheEnd = /\\[[^\\[]+\\]\\s*$/;\r\n    const ROOT_SVG_TAGS = new Set([\"svg\", \"g\", \"path\"]);\r\n    function parseDOMNode(node, ctx) {\r\n        const { tagName } = node;\r\n        const dynamicTag = node.getAttribute(\"t-tag\");\r\n        node.removeAttribute(\"t-tag\");\r\n        if (tagName === \"t\" && !dynamicTag) {\r\n            return null;\r\n        }\r\n        if (tagName.startsWith(\"block-\")) {\r\n            throw new OwlError(`Invalid tag name: '${tagName}'`);\r\n        }\r\n        ctx = Object.assign({}, ctx);\r\n        if (tagName === \"pre\") {\r\n            ctx.inPreTag = true;\r\n        }\r\n        let ns = !ctx.nameSpace && ROOT_SVG_TAGS.has(tagName) ? \"http://www.w3.org/2000/svg\" : null;\r\n        const ref = node.getAttribute(\"t-ref\");\r\n        node.removeAttribute(\"t-ref\");\r\n        const nodeAttrsNames = node.getAttributeNames();\r\n        let attrs = null;\r\n        let on = null;\r\n        let model = null;\r\n        for (let attr of nodeAttrsNames) {\r\n            const value = node.getAttribute(attr);\r\n            if (attr === \"t-on\" || attr === \"t-on-\") {\r\n                throw new OwlError(\"Missing event name with t-on directive\");\r\n            }\r\n            if (attr.startsWith(\"t-on-\")) {\r\n                on = on || {};\r\n                on[attr.slice(5)] = value;\r\n            }\r\n            else if (attr.startsWith(\"t-model\")) {\r\n                if (![\"input\", \"select\", \"textarea\"].includes(tagName)) {\r\n                    throw new OwlError(\"The t-model directive only works with <input>, <textarea> and <select>\");\r\n                }\r\n                let baseExpr, expr;\r\n                if (hasDotAtTheEnd.test(value)) {\r\n                    const index = value.lastIndexOf(\".\");\r\n                    baseExpr = value.slice(0, index);\r\n                    expr = `'${value.slice(index + 1)}'`;\r\n                }\r\n                else if (hasBracketsAtTheEnd.test(value)) {\r\n                    const index = value.lastIndexOf(\"[\");\r\n                    baseExpr = value.slice(0, index);\r\n                    expr = value.slice(index + 1, -1);\r\n                }\r\n                else {\r\n                    throw new OwlError(`Invalid t-model expression: \"${value}\" (it should be assignable)`);\r\n                }\r\n                const typeAttr = node.getAttribute(\"type\");\r\n                const isInput = tagName === \"input\";\r\n                const isSelect = tagName === \"select\";\r\n                const isCheckboxInput = isInput && typeAttr === \"checkbox\";\r\n                const isRadioInput = isInput && typeAttr === \"radio\";\r\n                const hasTrimMod = attr.includes(\".trim\");\r\n                const hasLazyMod = hasTrimMod || attr.includes(\".lazy\");\r\n                const hasNumberMod = attr.includes(\".number\");\r\n                const eventType = isRadioInput ? \"click\" : isSelect || hasLazyMod ? \"change\" : \"input\";\r\n                model = {\r\n                    baseExpr,\r\n                    expr,\r\n                    targetAttr: isCheckboxInput ? \"checked\" : \"value\",\r\n                    specialInitTargetAttr: isRadioInput ? \"checked\" : null,\r\n                    eventType,\r\n                    hasDynamicChildren: false,\r\n                    shouldTrim: hasTrimMod,\r\n                    shouldNumberize: hasNumberMod,\r\n                };\r\n                if (isSelect) {\r\n                    // don't pollute the original ctx\r\n                    ctx = Object.assign({}, ctx);\r\n                    ctx.tModelInfo = model;\r\n                }\r\n            }\r\n            else if (attr.startsWith(\"block-\")) {\r\n                throw new OwlError(`Invalid attribute: '${attr}'`);\r\n            }\r\n            else if (attr === \"xmlns\") {\r\n                ns = value;\r\n            }\r\n            else if (attr !== \"t-name\") {\r\n                if (attr.startsWith(\"t-\") && !attr.startsWith(\"t-att\")) {\r\n                    throw new OwlError(`Unknown QWeb directive: '${attr}'`);\r\n                }\r\n                const tModel = ctx.tModelInfo;\r\n                if (tModel && [\"t-att-value\", \"t-attf-value\"].includes(attr)) {\r\n                    tModel.hasDynamicChildren = true;\r\n                }\r\n                attrs = attrs || {};\r\n                attrs[attr] = value;\r\n            }\r\n        }\r\n        if (ns) {\r\n            ctx.nameSpace = ns;\r\n        }\r\n        const children = parseChildren(node, ctx);\r\n        return {\r\n            type: 2 /* DomNode */,\r\n            tag: tagName,\r\n            dynamicTag,\r\n            attrs,\r\n            on,\r\n            ref,\r\n            content: children,\r\n            model,\r\n            ns,\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // t-esc\r\n    // -----------------------------------------------------------------------------\r\n    function parseTEscNode(node, ctx) {\r\n        if (!node.hasAttribute(\"t-esc\")) {\r\n            return null;\r\n        }\r\n        const escValue = node.getAttribute(\"t-esc\");\r\n        node.removeAttribute(\"t-esc\");\r\n        const tesc = {\r\n            type: 4 /* TEsc */,\r\n            expr: escValue,\r\n            defaultValue: node.textContent || \"\",\r\n        };\r\n        let ref = node.getAttribute(\"t-ref\");\r\n        node.removeAttribute(\"t-ref\");\r\n        const ast = parseNode(node, ctx);\r\n        if (!ast) {\r\n            return tesc;\r\n        }\r\n        if (ast.type === 2 /* DomNode */) {\r\n            return {\r\n                ...ast,\r\n                ref,\r\n                content: [tesc],\r\n            };\r\n        }\r\n        return tesc;\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // t-out\r\n    // -----------------------------------------------------------------------------\r\n    function parseTOutNode(node, ctx) {\r\n        if (!node.hasAttribute(\"t-out\") && !node.hasAttribute(\"t-raw\")) {\r\n            return null;\r\n        }\r\n        if (node.hasAttribute(\"t-raw\")) {\r\n            console.warn(`t-raw has been deprecated in favor of t-out. If the value to render is not wrapped by the \"markup\" function, it will be escaped`);\r\n        }\r\n        const expr = (node.getAttribute(\"t-out\") || node.getAttribute(\"t-raw\"));\r\n        node.removeAttribute(\"t-out\");\r\n        node.removeAttribute(\"t-raw\");\r\n        const tOut = { type: 8 /* TOut */, expr, body: null };\r\n        const ref = node.getAttribute(\"t-ref\");\r\n        node.removeAttribute(\"t-ref\");\r\n        const ast = parseNode(node, ctx);\r\n        if (!ast) {\r\n            return tOut;\r\n        }\r\n        if (ast.type === 2 /* DomNode */) {\r\n            tOut.body = ast.content.length ? ast.content : null;\r\n            return {\r\n                ...ast,\r\n                ref,\r\n                content: [tOut],\r\n            };\r\n        }\r\n        return tOut;\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // t-foreach and t-key\r\n    // -----------------------------------------------------------------------------\r\n    function parseTForEach(node, ctx) {\r\n        if (!node.hasAttribute(\"t-foreach\")) {\r\n            return null;\r\n        }\r\n        const html = node.outerHTML;\r\n        const collection = node.getAttribute(\"t-foreach\");\r\n        node.removeAttribute(\"t-foreach\");\r\n        const elem = node.getAttribute(\"t-as\") || \"\";\r\n        node.removeAttribute(\"t-as\");\r\n        const key = node.getAttribute(\"t-key\");\r\n        if (!key) {\r\n            throw new OwlError(`\"Directive t-foreach should always be used with a t-key!\" (expression: t-foreach=\"${collection}\" t-as=\"${elem}\")`);\r\n        }\r\n        node.removeAttribute(\"t-key\");\r\n        const memo = node.getAttribute(\"t-memo\") || \"\";\r\n        node.removeAttribute(\"t-memo\");\r\n        const body = parseNode(node, ctx);\r\n        if (!body) {\r\n            return null;\r\n        }\r\n        const hasNoTCall = !html.includes(\"t-call\");\r\n        const hasNoFirst = hasNoTCall && !html.includes(`${elem}_first`);\r\n        const hasNoLast = hasNoTCall && !html.includes(`${elem}_last`);\r\n        const hasNoIndex = hasNoTCall && !html.includes(`${elem}_index`);\r\n        const hasNoValue = hasNoTCall && !html.includes(`${elem}_value`);\r\n        return {\r\n            type: 9 /* TForEach */,\r\n            collection,\r\n            elem,\r\n            body,\r\n            memo,\r\n            key,\r\n            hasNoFirst,\r\n            hasNoLast,\r\n            hasNoIndex,\r\n            hasNoValue,\r\n        };\r\n    }\r\n    function parseTKey(node, ctx) {\r\n        if (!node.hasAttribute(\"t-key\")) {\r\n            return null;\r\n        }\r\n        const key = node.getAttribute(\"t-key\");\r\n        node.removeAttribute(\"t-key\");\r\n        const body = parseNode(node, ctx);\r\n        if (!body) {\r\n            return null;\r\n        }\r\n        return { type: 10 /* TKey */, expr: key, content: body };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // t-call\r\n    // -----------------------------------------------------------------------------\r\n    function parseTCall(node, ctx) {\r\n        if (!node.hasAttribute(\"t-call\")) {\r\n            return null;\r\n        }\r\n        const subTemplate = node.getAttribute(\"t-call\");\r\n        const context = node.getAttribute(\"t-call-context\");\r\n        node.removeAttribute(\"t-call\");\r\n        node.removeAttribute(\"t-call-context\");\r\n        if (node.tagName !== \"t\") {\r\n            const ast = parseNode(node, ctx);\r\n            const tcall = { type: 7 /* TCall */, name: subTemplate, body: null, context };\r\n            if (ast && ast.type === 2 /* DomNode */) {\r\n                ast.content = [tcall];\r\n                return ast;\r\n            }\r\n            if (ast && ast.type === 11 /* TComponent */) {\r\n                return {\r\n                    ...ast,\r\n                    slots: { default: { content: tcall, scope: null, on: null, attrs: null } },\r\n                };\r\n            }\r\n        }\r\n        const body = parseChildren(node, ctx);\r\n        return {\r\n            type: 7 /* TCall */,\r\n            name: subTemplate,\r\n            body: body.length ? body : null,\r\n            context,\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // t-call-block\r\n    // -----------------------------------------------------------------------------\r\n    function parseTCallBlock(node, ctx) {\r\n        if (!node.hasAttribute(\"t-call-block\")) {\r\n            return null;\r\n        }\r\n        const name = node.getAttribute(\"t-call-block\");\r\n        return {\r\n            type: 15 /* TCallBlock */,\r\n            name,\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // t-if\r\n    // -----------------------------------------------------------------------------\r\n    function parseTIf(node, ctx) {\r\n        if (!node.hasAttribute(\"t-if\")) {\r\n            return null;\r\n        }\r\n        const condition = node.getAttribute(\"t-if\");\r\n        node.removeAttribute(\"t-if\");\r\n        const content = parseNode(node, ctx) || { type: 0 /* Text */, value: \"\" };\r\n        let nextElement = node.nextElementSibling;\r\n        // t-elifs\r\n        const tElifs = [];\r\n        while (nextElement && nextElement.hasAttribute(\"t-elif\")) {\r\n            const condition = nextElement.getAttribute(\"t-elif\");\r\n            nextElement.removeAttribute(\"t-elif\");\r\n            const tElif = parseNode(nextElement, ctx);\r\n            const next = nextElement.nextElementSibling;\r\n            nextElement.remove();\r\n            nextElement = next;\r\n            if (tElif) {\r\n                tElifs.push({ condition, content: tElif });\r\n            }\r\n        }\r\n        // t-else\r\n        let tElse = null;\r\n        if (nextElement && nextElement.hasAttribute(\"t-else\")) {\r\n            nextElement.removeAttribute(\"t-else\");\r\n            tElse = parseNode(nextElement, ctx);\r\n            nextElement.remove();\r\n        }\r\n        return {\r\n            type: 5 /* TIf */,\r\n            condition,\r\n            content,\r\n            tElif: tElifs.length ? tElifs : null,\r\n            tElse,\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // t-set directive\r\n    // -----------------------------------------------------------------------------\r\n    function parseTSetNode(node, ctx) {\r\n        if (!node.hasAttribute(\"t-set\")) {\r\n            return null;\r\n        }\r\n        const name = node.getAttribute(\"t-set\");\r\n        const value = node.getAttribute(\"t-value\") || null;\r\n        const defaultValue = node.innerHTML === node.textContent ? node.textContent || null : null;\r\n        let body = null;\r\n        if (node.textContent !== node.innerHTML) {\r\n            body = parseChildren(node, ctx);\r\n        }\r\n        return { type: 6 /* TSet */, name, value, defaultValue, body };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Components\r\n    // -----------------------------------------------------------------------------\r\n    // Error messages when trying to use an unsupported directive on a component\r\n    const directiveErrorMap = new Map([\r\n        [\r\n            \"t-ref\",\r\n            \"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.\",\r\n        ],\r\n        [\"t-att\", \"t-att makes no sense on component: props are already treated as expressions\"],\r\n        [\r\n            \"t-attf\",\r\n            \"t-attf is not supported on components: use template strings for string interpolation in props\",\r\n        ],\r\n    ]);\r\n    function parseComponent(node, ctx) {\r\n        let name = node.tagName;\r\n        const firstLetter = name[0];\r\n        let isDynamic = node.hasAttribute(\"t-component\");\r\n        if (isDynamic && name !== \"t\") {\r\n            throw new OwlError(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);\r\n        }\r\n        if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {\r\n            return null;\r\n        }\r\n        if (isDynamic) {\r\n            name = node.getAttribute(\"t-component\");\r\n            node.removeAttribute(\"t-component\");\r\n        }\r\n        const dynamicProps = node.getAttribute(\"t-props\");\r\n        node.removeAttribute(\"t-props\");\r\n        const defaultSlotScope = node.getAttribute(\"t-slot-scope\");\r\n        node.removeAttribute(\"t-slot-scope\");\r\n        let on = null;\r\n        let props = null;\r\n        for (let name of node.getAttributeNames()) {\r\n            const value = node.getAttribute(name);\r\n            if (name.startsWith(\"t-\")) {\r\n                if (name.startsWith(\"t-on-\")) {\r\n                    on = on || {};\r\n                    on[name.slice(5)] = value;\r\n                }\r\n                else {\r\n                    const message = directiveErrorMap.get(name.split(\"-\").slice(0, 2).join(\"-\"));\r\n                    throw new OwlError(message || `unsupported directive on Component: ${name}`);\r\n                }\r\n            }\r\n            else {\r\n                props = props || {};\r\n                props[name] = value;\r\n            }\r\n        }\r\n        let slots = null;\r\n        if (node.hasChildNodes()) {\r\n            const clone = node.cloneNode(true);\r\n            // named slots\r\n            const slotNodes = Array.from(clone.querySelectorAll(\"[t-set-slot]\"));\r\n            for (let slotNode of slotNodes) {\r\n                if (slotNode.tagName !== \"t\") {\r\n                    throw new OwlError(`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`);\r\n                }\r\n                const name = slotNode.getAttribute(\"t-set-slot\");\r\n                // check if this is defined in a sub component (in which case it should\r\n                // be ignored)\r\n                let el = slotNode.parentElement;\r\n                let isInSubComponent = false;\r\n                while (el && el !== clone) {\r\n                    if (el.hasAttribute(\"t-component\") || el.tagName[0] === el.tagName[0].toUpperCase()) {\r\n                        isInSubComponent = true;\r\n                        break;\r\n                    }\r\n                    el = el.parentElement;\r\n                }\r\n                if (isInSubComponent || !el) {\r\n                    continue;\r\n                }\r\n                slotNode.removeAttribute(\"t-set-slot\");\r\n                slotNode.remove();\r\n                const slotAst = parseNode(slotNode, ctx);\r\n                let on = null;\r\n                let attrs = null;\r\n                let scope = null;\r\n                for (let attributeName of slotNode.getAttributeNames()) {\r\n                    const value = slotNode.getAttribute(attributeName);\r\n                    if (attributeName === \"t-slot-scope\") {\r\n                        scope = value;\r\n                        continue;\r\n                    }\r\n                    else if (attributeName.startsWith(\"t-on-\")) {\r\n                        on = on || {};\r\n                        on[attributeName.slice(5)] = value;\r\n                    }\r\n                    else {\r\n                        attrs = attrs || {};\r\n                        attrs[attributeName] = value;\r\n                    }\r\n                }\r\n                slots = slots || {};\r\n                slots[name] = { content: slotAst, on, attrs, scope };\r\n            }\r\n            // default slot\r\n            const defaultContent = parseChildNodes(clone, ctx);\r\n            slots = slots || {};\r\n            // t-set-slot=\"default\" has priority over content\r\n            if (defaultContent && !slots.default) {\r\n                slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };\r\n            }\r\n        }\r\n        return { type: 11 /* TComponent */, name, isDynamic, dynamicProps, props, slots, on };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Slots\r\n    // -----------------------------------------------------------------------------\r\n    function parseTSlot(node, ctx) {\r\n        if (!node.hasAttribute(\"t-slot\")) {\r\n            return null;\r\n        }\r\n        const name = node.getAttribute(\"t-slot\");\r\n        node.removeAttribute(\"t-slot\");\r\n        let attrs = null;\r\n        let on = null;\r\n        for (let attributeName of node.getAttributeNames()) {\r\n            const value = node.getAttribute(attributeName);\r\n            if (attributeName.startsWith(\"t-on-\")) {\r\n                on = on || {};\r\n                on[attributeName.slice(5)] = value;\r\n            }\r\n            else {\r\n                attrs = attrs || {};\r\n                attrs[attributeName] = value;\r\n            }\r\n        }\r\n        return {\r\n            type: 14 /* TSlot */,\r\n            name,\r\n            attrs,\r\n            on,\r\n            defaultContent: parseChildNodes(node, ctx),\r\n        };\r\n    }\r\n    function parseTTranslation(node, ctx) {\r\n        if (node.getAttribute(\"t-translation\") !== \"off\") {\r\n            return null;\r\n        }\r\n        node.removeAttribute(\"t-translation\");\r\n        return {\r\n            type: 16 /* TTranslation */,\r\n            content: parseNode(node, ctx),\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Portal\r\n    // -----------------------------------------------------------------------------\r\n    function parseTPortal(node, ctx) {\r\n        if (!node.hasAttribute(\"t-portal\")) {\r\n            return null;\r\n        }\r\n        const target = node.getAttribute(\"t-portal\");\r\n        node.removeAttribute(\"t-portal\");\r\n        const content = parseNode(node, ctx);\r\n        if (!content) {\r\n            return {\r\n                type: 0 /* Text */,\r\n                value: \"\",\r\n            };\r\n        }\r\n        return {\r\n            type: 17 /* TPortal */,\r\n            target,\r\n            content,\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // helpers\r\n    // -----------------------------------------------------------------------------\r\n    /**\r\n     * Parse all the child nodes of a given node and return a list of ast elements\r\n     */\r\n    function parseChildren(node, ctx) {\r\n        const children = [];\r\n        for (let child of node.childNodes) {\r\n            const childAst = parseNode(child, ctx);\r\n            if (childAst) {\r\n                if (childAst.type === 3 /* Multi */) {\r\n                    children.push(...childAst.content);\r\n                }\r\n                else {\r\n                    children.push(childAst);\r\n                }\r\n            }\r\n        }\r\n        return children;\r\n    }\r\n    /**\r\n     * Parse all the child nodes of a given node and return an ast if possible.\r\n     * In the case there are multiple children, they are wrapped in a astmulti.\r\n     */\r\n    function parseChildNodes(node, ctx) {\r\n        const children = parseChildren(node, ctx);\r\n        switch (children.length) {\r\n            case 0:\r\n                return null;\r\n            case 1:\r\n                return children[0];\r\n            default:\r\n                return { type: 3 /* Multi */, content: children };\r\n        }\r\n    }\r\n    /**\r\n     * Normalizes the content of an Element so that t-if/t-elif/t-else directives\r\n     * immediately follow one another (by removing empty text nodes or comments).\r\n     * Throws an error when a conditional branching statement is malformed. This\r\n     * function modifies the Element in place.\r\n     *\r\n     * @param el the element containing the tree that should be normalized\r\n     */\r\n    function normalizeTIf(el) {\r\n        let tbranch = el.querySelectorAll(\"[t-elif], [t-else]\");\r\n        for (let i = 0, ilen = tbranch.length; i < ilen; i++) {\r\n            let node = tbranch[i];\r\n            let prevElem = node.previousElementSibling;\r\n            let pattr = (name) => prevElem.getAttribute(name);\r\n            let nattr = (name) => +!!node.getAttribute(name);\r\n            if (prevElem && (pattr(\"t-if\") || pattr(\"t-elif\"))) {\r\n                if (pattr(\"t-foreach\")) {\r\n                    throw new OwlError(\"t-if cannot stay at the same level as t-foreach when using t-elif or t-else\");\r\n                }\r\n                if ([\"t-if\", \"t-elif\", \"t-else\"].map(nattr).reduce(function (a, b) {\r\n                    return a + b;\r\n                }) > 1) {\r\n                    throw new OwlError(\"Only one conditional branching directive is allowed per node\");\r\n                }\r\n                // All text (with only spaces) and comment nodes (nodeType 8) between\r\n                // branch nodes are removed\r\n                let textNode;\r\n                while ((textNode = node.previousSibling) !== prevElem) {\r\n                    if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {\r\n                        throw new OwlError(\"text is not allowed between branching directives\");\r\n                    }\r\n                    textNode.remove();\r\n                }\r\n            }\r\n            else {\r\n                throw new OwlError(\"t-elif and t-else directives must be preceded by a t-if or t-elif directive\");\r\n            }\r\n        }\r\n    }\r\n    /**\r\n     * Normalizes the content of an Element so that t-esc directives on components\r\n     * are removed and instead places a <t t-esc=\"\"> as the default slot of the\r\n     * component. Also throws if the component already has content. This function\r\n     * modifies the Element in place.\r\n     *\r\n     * @param el the element containing the tree that should be normalized\r\n     */\r\n    function normalizeTEscTOut(el) {\r\n        for (const d of [\"t-esc\", \"t-out\"]) {\r\n            const elements = [...el.querySelectorAll(`[${d}]`)].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute(\"t-component\"));\r\n            for (const el of elements) {\r\n                if (el.childNodes.length) {\r\n                    throw new OwlError(`Cannot have ${d} on a component that already has content`);\r\n                }\r\n                const value = el.getAttribute(d);\r\n                el.removeAttribute(d);\r\n                const t = el.ownerDocument.createElement(\"t\");\r\n                if (value != null) {\r\n                    t.setAttribute(d, value);\r\n                }\r\n                el.appendChild(t);\r\n            }\r\n        }\r\n    }\r\n    /**\r\n     * Normalizes the tree inside a given element and do some preliminary validation\r\n     * on it. This function modifies the Element in place.\r\n     *\r\n     * @param el the element containing the tree that should be normalized\r\n     */\r\n    function normalizeXML(el) {\r\n        normalizeTIf(el);\r\n        normalizeTEscTOut(el);\r\n    }\r\n\r\n    function compile(template, options = {}) {\r\n        // parsing\r\n        const ast = parse(template);\r\n        // some work\r\n        const hasSafeContext = template instanceof Node\r\n            ? !(template instanceof Element) || template.querySelector(\"[t-set], [t-call]\") === null\r\n            : !template.includes(\"t-set\") && !template.includes(\"t-call\");\r\n        // code generation\r\n        const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });\r\n        const code = codeGenerator.generateCode();\r\n        // template function\r\n        try {\r\n            return new Function(\"app, bdom, helpers\", code);\r\n        }\r\n        catch (originalError) {\r\n            const { name } = options;\r\n            const nameStr = name ? `template \"${name}\"` : \"anonymous template\";\r\n            const err = new OwlError(`Failed to compile ${nameStr}: ${originalError.message}\\n\\ngenerated code:\\nfunction(app, bdom, helpers) {\\n${code}\\n}`);\r\n            err.cause = originalError;\r\n            throw err;\r\n        }\r\n    }\r\n\r\n    // do not modify manually. This file is generated by the release script.\r\n    const version = \"2.4.0\";\r\n\r\n    // -----------------------------------------------------------------------------\r\n    //  Scheduler\r\n    // -----------------------------------------------------------------------------\r\n    class Scheduler {\r\n        constructor() {\r\n            this.tasks = new Set();\r\n            this.frame = 0;\r\n            this.delayedRenders = [];\r\n            this.cancelledNodes = new Set();\r\n            this.requestAnimationFrame = Scheduler.requestAnimationFrame;\r\n        }\r\n        addFiber(fiber) {\r\n            this.tasks.add(fiber.root);\r\n        }\r\n        scheduleDestroy(node) {\r\n            this.cancelledNodes.add(node);\r\n            if (this.frame === 0) {\r\n                this.frame = this.requestAnimationFrame(() => this.processTasks());\r\n            }\r\n        }\r\n        /**\r\n         * Process all current tasks. This only applies to the fibers that are ready.\r\n         * Other tasks are left unchanged.\r\n         */\r\n        flush() {\r\n            if (this.delayedRenders.length) {\r\n                let renders = this.delayedRenders;\r\n                this.delayedRenders = [];\r\n                for (let f of renders) {\r\n                    if (f.root && f.node.status !== 3 /* DESTROYED */ && f.node.fiber === f) {\r\n                        f.render();\r\n                    }\r\n                }\r\n            }\r\n            if (this.frame === 0) {\r\n                this.frame = this.requestAnimationFrame(() => this.processTasks());\r\n            }\r\n        }\r\n        processTasks() {\r\n            this.frame = 0;\r\n            for (let node of this.cancelledNodes) {\r\n                node._destroy();\r\n            }\r\n            this.cancelledNodes.clear();\r\n            for (let task of this.tasks) {\r\n                this.processFiber(task);\r\n            }\r\n            for (let task of this.tasks) {\r\n                if (task.node.status === 3 /* DESTROYED */) {\r\n                    this.tasks.delete(task);\r\n                }\r\n            }\r\n        }\r\n        processFiber(fiber) {\r\n            if (fiber.root !== fiber) {\r\n                this.tasks.delete(fiber);\r\n                return;\r\n            }\r\n            const hasError = fibersInError.has(fiber);\r\n            if (hasError && fiber.counter !== 0) {\r\n                this.tasks.delete(fiber);\r\n                return;\r\n            }\r\n            if (fiber.node.status === 3 /* DESTROYED */) {\r\n                this.tasks.delete(fiber);\r\n                return;\r\n            }\r\n            if (fiber.counter === 0) {\r\n                if (!hasError) {\r\n                    fiber.complete();\r\n                }\r\n                this.tasks.delete(fiber);\r\n            }\r\n        }\r\n    }\r\n    // capture the value of requestAnimationFrame as soon as possible, to avoid\r\n    // interactions with other code, such as test frameworks that override them\r\n    Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);\r\n\r\n    let hasBeenLogged = false;\r\n    const DEV_MSG = () => {\r\n        const hash = window.owl ? window.owl.__info__.hash : \"master\";\r\n        return `Owl is running in 'dev' mode.\r\n\r\nThis is not suitable for production use.\r\nSee https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;\r\n    };\r\n    const apps = new Set();\r\n    window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = { apps, Fiber, RootFiber, toRaw, reactive });\r\n    class App extends TemplateSet {\r\n        constructor(Root, config = {}) {\r\n            super(config);\r\n            this.scheduler = new Scheduler();\r\n            this.subRoots = new Set();\r\n            this.root = null;\r\n            this.name = config.name || \"\";\r\n            this.Root = Root;\r\n            apps.add(this);\r\n            if (config.test) {\r\n                this.dev = true;\r\n            }\r\n            this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;\r\n            if (this.dev && !config.test && !hasBeenLogged) {\r\n                console.info(DEV_MSG());\r\n                hasBeenLogged = true;\r\n            }\r\n            const env = config.env || {};\r\n            const descrs = Object.getOwnPropertyDescriptors(env);\r\n            this.env = Object.freeze(Object.create(Object.getPrototypeOf(env), descrs));\r\n            this.props = config.props || {};\r\n        }\r\n        mount(target, options) {\r\n            const root = this.createRoot(this.Root, { props: this.props });\r\n            this.root = root.node;\r\n            this.subRoots.delete(root.node);\r\n            return root.mount(target, options);\r\n        }\r\n        createRoot(Root, config = {}) {\r\n            const props = config.props || {};\r\n            // hack to make sure the sub root get the sub env if necessary. for owl 3,\r\n            // would be nice to rethink the initialization process to make sure that\r\n            // we can create a ComponentNode and give it explicitely the env, instead\r\n            // of looking it up in the app\r\n            const env = this.env;\r\n            if (config.env) {\r\n                this.env = config.env;\r\n            }\r\n            const node = this.makeNode(Root, props);\r\n            if (config.env) {\r\n                this.env = env;\r\n            }\r\n            this.subRoots.add(node);\r\n            return {\r\n                node,\r\n                mount: (target, options) => {\r\n                    App.validateTarget(target);\r\n                    if (this.dev) {\r\n                        validateProps(Root, props, { __owl__: { app: this } });\r\n                    }\r\n                    const prom = this.mountNode(node, target, options);\r\n                    return prom;\r\n                },\r\n                destroy: () => {\r\n                    this.subRoots.delete(node);\r\n                    node.destroy();\r\n                    this.scheduler.processTasks();\r\n                },\r\n            };\r\n        }\r\n        makeNode(Component, props) {\r\n            return new ComponentNode(Component, props, this, null, null);\r\n        }\r\n        mountNode(node, target, options) {\r\n            const promise = new Promise((resolve, reject) => {\r\n                let isResolved = false;\r\n                // manually set a onMounted callback.\r\n                // that way, we are independant from the current node.\r\n                node.mounted.push(() => {\r\n                    resolve(node.component);\r\n                    isResolved = true;\r\n                });\r\n                // Manually add the last resort error handler on the node\r\n                let handlers = nodeErrorHandlers.get(node);\r\n                if (!handlers) {\r\n                    handlers = [];\r\n                    nodeErrorHandlers.set(node, handlers);\r\n                }\r\n                handlers.unshift((e) => {\r\n                    if (!isResolved) {\r\n                        reject(e);\r\n                    }\r\n                    throw e;\r\n                });\r\n            });\r\n            node.mountComponent(target, options);\r\n            return promise;\r\n        }\r\n        destroy() {\r\n            if (this.root) {\r\n                for (let subroot of this.subRoots) {\r\n                    subroot.destroy();\r\n                }\r\n                this.root.destroy();\r\n                this.scheduler.processTasks();\r\n            }\r\n            apps.delete(this);\r\n        }\r\n        createComponent(name, isStatic, hasSlotsProp, hasDynamicPropList, propList) {\r\n            const isDynamic = !isStatic;\r\n            let arePropsDifferent;\r\n            const hasNoProp = propList.length === 0;\r\n            if (hasSlotsProp) {\r\n                arePropsDifferent = (_1, _2) => true;\r\n            }\r\n            else if (hasDynamicPropList) {\r\n                arePropsDifferent = function (props1, props2) {\r\n                    for (let k in props1) {\r\n                        if (props1[k] !== props2[k]) {\r\n                            return true;\r\n                        }\r\n                    }\r\n                    return Object.keys(props1).length !== Object.keys(props2).length;\r\n                };\r\n            }\r\n            else if (hasNoProp) {\r\n                arePropsDifferent = (_1, _2) => false;\r\n            }\r\n            else {\r\n                arePropsDifferent = function (props1, props2) {\r\n                    for (let p of propList) {\r\n                        if (props1[p] !== props2[p]) {\r\n                            return true;\r\n                        }\r\n                    }\r\n                    return false;\r\n                };\r\n            }\r\n            const updateAndRender = ComponentNode.prototype.updateAndRender;\r\n            const initiateRender = ComponentNode.prototype.initiateRender;\r\n            return (props, key, ctx, parent, C) => {\r\n                let children = ctx.children;\r\n                let node = children[key];\r\n                if (isDynamic && node && node.component.constructor !== C) {\r\n                    node = undefined;\r\n                }\r\n                const parentFiber = ctx.fiber;\r\n                if (node) {\r\n                    if (arePropsDifferent(node.props, props) || parentFiber.deep || node.forceNextRender) {\r\n                        node.forceNextRender = false;\r\n                        updateAndRender.call(node, props, parentFiber);\r\n                    }\r\n                }\r\n                else {\r\n                    // new component\r\n                    if (isStatic) {\r\n                        const components = parent.constructor.components;\r\n                        if (!components) {\r\n                            throw new OwlError(`Cannot find the definition of component \"${name}\", missing static components key in parent`);\r\n                        }\r\n                        C = components[name];\r\n                        if (!C) {\r\n                            throw new OwlError(`Cannot find the definition of component \"${name}\"`);\r\n                        }\r\n                        else if (!(C.prototype instanceof Component)) {\r\n                            throw new OwlError(`\"${name}\" is not a Component. It must inherit from the Component class`);\r\n                        }\r\n                    }\r\n                    node = new ComponentNode(C, props, this, ctx, key);\r\n                    children[key] = node;\r\n                    initiateRender.call(node, new Fiber(node, parentFiber));\r\n                }\r\n                parentFiber.childrenMap[key] = node;\r\n                return node;\r\n            };\r\n        }\r\n        handleError(...args) {\r\n            return handleError(...args);\r\n        }\r\n    }\r\n    App.validateTarget = validateTarget;\r\n    App.apps = apps;\r\n    App.version = version;\r\n    async function mount(C, target, config = {}) {\r\n        return new App(C, config).mount(target, config);\r\n    }\r\n\r\n    const mainEventHandler = (data, ev, currentTarget) => {\r\n        const { data: _data, modifiers } = filterOutModifiersFromData(data);\r\n        data = _data;\r\n        let stopped = false;\r\n        if (modifiers.length) {\r\n            let selfMode = false;\r\n            const isSelf = ev.target === currentTarget;\r\n            for (const mod of modifiers) {\r\n                switch (mod) {\r\n                    case \"self\":\r\n                        selfMode = true;\r\n                        if (isSelf) {\r\n                            continue;\r\n                        }\r\n                        else {\r\n                            return stopped;\r\n                        }\r\n                    case \"prevent\":\r\n                        if ((selfMode && isSelf) || !selfMode)\r\n                            ev.preventDefault();\r\n                        continue;\r\n                    case \"stop\":\r\n                        if ((selfMode && isSelf) || !selfMode)\r\n                            ev.stopPropagation();\r\n                        stopped = true;\r\n                        continue;\r\n                }\r\n            }\r\n        }\r\n        // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0\r\n        // We check this rather than data[0] being truthy (or typeof function) so that it crashes\r\n        // as expected when there is a handler expression that evaluates to a falsy value\r\n        if (Object.hasOwnProperty.call(data, 0)) {\r\n            const handler = data[0];\r\n            if (typeof handler !== \"function\") {\r\n                throw new OwlError(`Invalid handler (expected a function, received: '${handler}')`);\r\n            }\r\n            let node = data[1] ? data[1].__owl__ : null;\r\n            if (node ? node.status === 1 /* MOUNTED */ : true) {\r\n                handler.call(node ? node.component : null, ev);\r\n            }\r\n        }\r\n        return stopped;\r\n    };\r\n\r\n    function status(component) {\r\n        switch (component.__owl__.status) {\r\n            case 0 /* NEW */:\r\n                return \"new\";\r\n            case 2 /* CANCELLED */:\r\n                return \"cancelled\";\r\n            case 1 /* MOUNTED */:\r\n                return \"mounted\";\r\n            case 3 /* DESTROYED */:\r\n                return \"destroyed\";\r\n        }\r\n    }\r\n\r\n    // -----------------------------------------------------------------------------\r\n    // useRef\r\n    // -----------------------------------------------------------------------------\r\n    /**\r\n     * The purpose of this hook is to allow components to get a reference to a sub\r\n     * html node or component.\r\n     */\r\n    function useRef(name) {\r\n        const node = getCurrent();\r\n        const refs = node.refs;\r\n        return {\r\n            get el() {\r\n                const el = refs[name];\r\n                return inOwnerDocument(el) ? el : null;\r\n            },\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // useEnv and useSubEnv\r\n    // -----------------------------------------------------------------------------\r\n    /**\r\n     * This hook is useful as a building block for some customized hooks, that may\r\n     * need a reference to the env of the component calling them.\r\n     */\r\n    function useEnv() {\r\n        return getCurrent().component.env;\r\n    }\r\n    function extendEnv(currentEnv, extension) {\r\n        const env = Object.create(currentEnv);\r\n        const descrs = Object.getOwnPropertyDescriptors(extension);\r\n        return Object.freeze(Object.defineProperties(env, descrs));\r\n    }\r\n    /**\r\n     * This hook is a simple way to let components use a sub environment.  Note that\r\n     * like for all hooks, it is important that this is only called in the\r\n     * constructor method.\r\n     */\r\n    function useSubEnv(envExtension) {\r\n        const node = getCurrent();\r\n        node.component.env = extendEnv(node.component.env, envExtension);\r\n        useChildSubEnv(envExtension);\r\n    }\r\n    function useChildSubEnv(envExtension) {\r\n        const node = getCurrent();\r\n        node.childEnv = extendEnv(node.childEnv, envExtension);\r\n    }\r\n    /**\r\n     * This hook will run a callback when a component is mounted and patched, and\r\n     * will run a cleanup function before patching and before unmounting the\r\n     * the component.\r\n     *\r\n     * @template T\r\n     * @param {Effect<T>} effect the effect to run on component mount and/or patch\r\n     * @param {()=>[...T]} [computeDependencies=()=>[NaN]] a callback to compute\r\n     *      dependencies that will decide if the effect needs to be cleaned up and\r\n     *      run again. If the dependencies did not change, the effect will not run\r\n     *      again. The default value returns an array containing only NaN because\r\n     *      NaN !== NaN, which will cause the effect to rerun on every patch.\r\n     */\r\n    function useEffect(effect, computeDependencies = () => [NaN]) {\r\n        let cleanup;\r\n        let dependencies;\r\n        onMounted(() => {\r\n            dependencies = computeDependencies();\r\n            cleanup = effect(...dependencies);\r\n        });\r\n        onPatched(() => {\r\n            const newDeps = computeDependencies();\r\n            const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);\r\n            if (shouldReapply) {\r\n                dependencies = newDeps;\r\n                if (cleanup) {\r\n                    cleanup();\r\n                }\r\n                cleanup = effect(...dependencies);\r\n            }\r\n        });\r\n        onWillUnmount(() => cleanup && cleanup());\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // useExternalListener\r\n    // -----------------------------------------------------------------------------\r\n    /**\r\n     * When a component needs to listen to DOM Events on element(s) that are not\r\n     * part of his hierarchy, we can use the `useExternalListener` hook.\r\n     * It will correctly add and remove the event listener, whenever the\r\n     * component is mounted and unmounted.\r\n     *\r\n     * Example:\r\n     *  a menu needs to listen to the click on window to be closed automatically\r\n     *\r\n     * Usage:\r\n     *  in the constructor of the OWL component that needs to be notified,\r\n     *  `useExternalListener(window, 'click', this._doSomething);`\r\n     * */\r\n    function useExternalListener(target, eventName, handler, eventParams) {\r\n        const node = getCurrent();\r\n        const boundHandler = handler.bind(node.component);\r\n        onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));\r\n        onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));\r\n    }\r\n\r\n    config.shouldNormalizeDom = false;\r\n    config.mainEventHandler = mainEventHandler;\r\n    const blockDom = {\r\n        config,\r\n        // bdom entry points\r\n        mount: mount$1,\r\n        patch,\r\n        remove,\r\n        // bdom block types\r\n        list,\r\n        multi,\r\n        text,\r\n        toggler,\r\n        createBlock,\r\n        html,\r\n        comment,\r\n    };\r\n    const __info__ = {\r\n        version: App.version,\r\n    };\r\n\r\n    TemplateSet.prototype._compileTemplate = function _compileTemplate(name, template) {\r\n        return compile(template, {\r\n            name,\r\n            dev: this.dev,\r\n            translateFn: this.translateFn,\r\n            translatableAttributes: this.translatableAttributes,\r\n        });\r\n    };\r\n\r\n    exports.App = App;\r\n    exports.Component = Component;\r\n    exports.EventBus = EventBus;\r\n    exports.OwlError = OwlError;\r\n    exports.__info__ = __info__;\r\n    exports.batched = batched;\r\n    exports.blockDom = blockDom;\r\n    exports.loadFile = loadFile;\r\n    exports.markRaw = markRaw;\r\n    exports.markup = markup;\r\n    exports.mount = mount;\r\n    exports.onError = onError;\r\n    exports.onMounted = onMounted;\r\n    exports.onPatched = onPatched;\r\n    exports.onRendered = onRendered;\r\n    exports.onWillDestroy = onWillDestroy;\r\n    exports.onWillPatch = onWillPatch;\r\n    exports.onWillRender = onWillRender;\r\n    exports.onWillStart = onWillStart;\r\n    exports.onWillUnmount = onWillUnmount;\r\n    exports.onWillUpdateProps = onWillUpdateProps;\r\n    exports.reactive = reactive;\r\n    exports.status = status;\r\n    exports.toRaw = toRaw;\r\n    exports.useChildSubEnv = useChildSubEnv;\r\n    exports.useComponent = useComponent;\r\n    exports.useEffect = useEffect;\r\n    exports.useEnv = useEnv;\r\n    exports.useExternalListener = useExternalListener;\r\n    exports.useRef = useRef;\r\n    exports.useState = useState;\r\n    exports.useSubEnv = useSubEnv;\r\n    exports.validate = validate;\r\n    exports.validateType = validateType;\r\n    exports.whenReady = whenReady;\r\n    exports.xml = xml;\r\n\r\n    Object.defineProperty(exports, '__esModule', { value: true });\r\n\r\n\r\n    __info__.date = '2024-09-30T08:49:29.420Z';\r\n    __info__.hash = 'eb2b32a';\r\n    __info__.url = 'https://github.com/odoo/owl';\r\n\r\n\r\n})(this.owl = this.owl || {});\r\n", "odoo.define(\"@odoo/owl\", [], function () {\n    \"use strict\";\n\n    return owl;\n});\n", "/*!\n * jQuery JavaScript Library v3.6.3\n * https://jquery.com/\n *\n * Includes Sizzle.js\n * https://sizzlejs.com/\n *\n * Copyright OpenJS Foundation and other contributors\n * Released under the MIT license\n * https://jquery.org/license\n *\n * Date: 2022-12-20T21:28Z\n */\n( function( global, factory ) {\n\n\t\"use strict\";\n\n\tif ( typeof module === \"object\" && typeof module.exports === \"object\" ) {\n\n\t\t// For CommonJS and CommonJS-like environments where a proper `window`\n\t\t// is present, execute the factory and get jQuery.\n\t\t// For environments that do not have a `window` with a `document`\n\t\t// (such as Node.js), expose a factory as module.exports.\n\t\t// This accentuates the need for the creation of a real `window`.\n\t\t// e.g. var jQuery = require(\"jquery\")(window);\n\t\t// See ticket trac-14549 for more info.\n\t\tmodule.exports = global.document ?\n\t\t\tfactory( global, true ) :\n\t\t\tfunction( w ) {\n\t\t\t\tif ( !w.document ) {\n\t\t\t\t\tthrow new Error( \"jQuery requires a window with a document\" );\n\t\t\t\t}\n\t\t\t\treturn factory( w );\n\t\t\t};\n\t} else {\n\t\tfactory( global );\n\t}\n\n// Pass this if window is not defined yet\n} )( typeof window !== \"undefined\" ? window : this, function( window, noGlobal ) {\n\n// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1\n// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode\n// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common\n// enough that all such attempts are guarded in a try block.\n\"use strict\";\n\nvar arr = [];\n\nvar getProto = Object.getPrototypeOf;\n\nvar slice = arr.slice;\n\nvar flat = arr.flat ? function( array ) {\n\treturn arr.flat.call( array );\n} : function( array ) {\n\treturn arr.concat.apply( [], array );\n};\n\n\nvar push = arr.push;\n\nvar indexOf = arr.indexOf;\n\nvar class2type = {};\n\nvar toString = class2type.toString;\n\nvar hasOwn = class2type.hasOwnProperty;\n\nvar fnToString = hasOwn.toString;\n\nvar ObjectFunctionString = fnToString.call( Object );\n\nvar support = {};\n\nvar isFunction = function isFunction( obj ) {\n\n\t\t// Support: Chrome <=57, Firefox <=52\n\t\t// In some browsers, typeof returns \"function\" for HTML <object> elements\n\t\t// (i.e., `typeof document.createElement( \"object\" ) === \"function\"`).\n\t\t// We don't want to classify *any* DOM node as a function.\n\t\t// Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5\n\t\t// Plus for old WebKit, typeof returns \"function\" for HTML collections\n\t\t// (e.g., `typeof document.getElementsByTagName(\"div\") === \"function\"`). (gh-4756)\n\t\treturn typeof obj === \"function\" && typeof obj.nodeType !== \"number\" &&\n\t\t\ttypeof obj.item !== \"function\";\n\t};\n\n\nvar isWindow = function isWindow( obj ) {\n\t\treturn obj != null && obj === obj.window;\n\t};\n\n\nvar document = window.document;\n\n\n\n\tvar preservedScriptAttributes = {\n\t\ttype: true,\n\t\tsrc: true,\n\t\tnonce: true,\n\t\tnoModule: true\n\t};\n\n\tfunction DOMEval( code, node, doc ) {\n\t\tdoc = doc || document;\n\n\t\tvar i, val,\n\t\t\tscript = doc.createElement( \"script\" );\n\n\t\tscript.text = code;\n\t\tif ( node ) {\n\t\t\tfor ( i in preservedScriptAttributes ) {\n\n\t\t\t\t// Support: Firefox 64+, Edge 18+\n\t\t\t\t// Some browsers don't support the \"nonce\" property on scripts.\n\t\t\t\t// On the other hand, just using `getAttribute` is not enough as\n\t\t\t\t// the `nonce` attribute is reset to an empty string whenever it\n\t\t\t\t// becomes browsing-context connected.\n\t\t\t\t// See https://github.com/whatwg/html/issues/2369\n\t\t\t\t// See https://html.spec.whatwg.org/#nonce-attributes\n\t\t\t\t// The `node.getAttribute` check was added for the sake of\n\t\t\t\t// `jQuery.globalEval` so that it can fake a nonce-containing node\n\t\t\t\t// via an object.\n\t\t\t\tval = node[ i ] || node.getAttribute && node.getAttribute( i );\n\t\t\t\tif ( val ) {\n\t\t\t\t\tscript.setAttribute( i, val );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tdoc.head.appendChild( script ).parentNode.removeChild( script );\n\t}\n\n\nfunction toType( obj ) {\n\tif ( obj == null ) {\n\t\treturn obj + \"\";\n\t}\n\n\t// Support: Android <=2.3 only (functionish RegExp)\n\treturn typeof obj === \"object\" || typeof obj === \"function\" ?\n\t\tclass2type[ toString.call( obj ) ] || \"object\" :\n\t\ttypeof obj;\n}\n/* global Symbol */\n// Defining this global in .eslintrc.json would create a danger of using the global\n// unguarded in another place, it seems safer to define global only for this module\n\n\n\nvar\n\tversion = \"3.6.3\",\n\n\t// Define a local copy of jQuery\n\tjQuery = function( selector, context ) {\n\n\t\t// The jQuery object is actually just the init constructor 'enhanced'\n\t\t// Need init if jQuery is called (just allow error to be thrown if not included)\n\t\treturn new jQuery.fn.init( selector, context );\n\t};\n\njQuery.fn = jQuery.prototype = {\n\n\t// The current version of jQuery being used\n\tjquery: version,\n\n\tconstructor: jQuery,\n\n\t// The default length of a jQuery object is 0\n\tlength: 0,\n\n\ttoArray: function() {\n\t\treturn slice.call( this );\n\t},\n\n\t// Get the Nth element in the matched element set OR\n\t// Get the whole matched element set as a clean array\n\tget: function( num ) {\n\n\t\t// Return all the elements in a clean array\n\t\tif ( num == null ) {\n\t\t\treturn slice.call( this );\n\t\t}\n\n\t\t// Return just the one element from the set\n\t\treturn num < 0 ? this[ num + this.length ] : this[ num ];\n\t},\n\n\t// Take an array of elements and push it onto the stack\n\t// (returning the new matched element set)\n\tpushStack: function( elems ) {\n\n\t\t// Build a new jQuery matched element set\n\t\tvar ret = jQuery.merge( this.constructor(), elems );\n\n\t\t// Add the old object onto the stack (as a reference)\n\t\tret.prevObject = this;\n\n\t\t// Return the newly-formed element set\n\t\treturn ret;\n\t},\n\n\t// Execute a callback for every element in the matched set.\n\teach: function( callback ) {\n\t\treturn jQuery.each( this, callback );\n\t},\n\n\tmap: function( callback ) {\n\t\treturn this.pushStack( jQuery.map( this, function( elem, i ) {\n\t\t\treturn callback.call( elem, i, elem );\n\t\t} ) );\n\t},\n\n\tslice: function() {\n\t\treturn this.pushStack( slice.apply( this, arguments ) );\n\t},\n\n\tfirst: function() {\n\t\treturn this.eq( 0 );\n\t},\n\n\tlast: function() {\n\t\treturn this.eq( -1 );\n\t},\n\n\teven: function() {\n\t\treturn this.pushStack( jQuery.grep( this, function( _elem, i ) {\n\t\t\treturn ( i + 1 ) % 2;\n\t\t} ) );\n\t},\n\n\todd: function() {\n\t\treturn this.pushStack( jQuery.grep( this, function( _elem, i ) {\n\t\t\treturn i % 2;\n\t\t} ) );\n\t},\n\n\teq: function( i ) {\n\t\tvar len = this.length,\n\t\t\tj = +i + ( i < 0 ? len : 0 );\n\t\treturn this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );\n\t},\n\n\tend: function() {\n\t\treturn this.prevObject || this.constructor();\n\t},\n\n\t// For internal use only.\n\t// Behaves like an Array's method, not like a jQuery method.\n\tpush: push,\n\tsort: arr.sort,\n\tsplice: arr.splice\n};\n\njQuery.extend = jQuery.fn.extend = function() {\n\tvar options, name, src, copy, copyIsArray, clone,\n\t\ttarget = arguments[ 0 ] || {},\n\t\ti = 1,\n\t\tlength = arguments.length,\n\t\tdeep = false;\n\n\t// Handle a deep copy situation\n\tif ( typeof target === \"boolean\" ) {\n\t\tdeep = target;\n\n\t\t// Skip the boolean and the target\n\t\ttarget = arguments[ i ] || {};\n\t\ti++;\n\t}\n\n\t// Handle case when target is a string or something (possible in deep copy)\n\tif ( typeof target !== \"object\" && !isFunction( target ) ) {\n\t\ttarget = {};\n\t}\n\n\t// Extend jQuery itself if only one argument is passed\n\tif ( i === length ) {\n\t\ttarget = this;\n\t\ti--;\n\t}\n\n\tfor ( ; i < length; i++ ) {\n\n\t\t// Only deal with non-null/undefined values\n\t\tif ( ( options = arguments[ i ] ) != null ) {\n\n\t\t\t// Extend the base object\n\t\t\tfor ( name in options ) {\n\t\t\t\tcopy = options[ name ];\n\n\t\t\t\t// Prevent Object.prototype pollution\n\t\t\t\t// Prevent never-ending loop\n\t\t\t\tif ( name === \"__proto__\" || target === copy ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Recurse if we're merging plain objects or arrays\n\t\t\t\tif ( deep && copy && ( jQuery.isPlainObject( copy ) ||\n\t\t\t\t\t( copyIsArray = Array.isArray( copy ) ) ) ) {\n\t\t\t\t\tsrc = target[ name ];\n\n\t\t\t\t\t// Ensure proper type for the source value\n\t\t\t\t\tif ( copyIsArray && !Array.isArray( src ) ) {\n\t\t\t\t\t\tclone = [];\n\t\t\t\t\t} else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {\n\t\t\t\t\t\tclone = {};\n\t\t\t\t\t} else {\n\t\t\t\t\t\tclone = src;\n\t\t\t\t\t}\n\t\t\t\t\tcopyIsArray = false;\n\n\t\t\t\t\t// Never move original objects, clone them\n\t\t\t\t\ttarget[ name ] = jQuery.extend( deep, clone, copy );\n\n\t\t\t\t// Don't bring in undefined values\n\t\t\t\t} else if ( copy !== undefined ) {\n\t\t\t\t\ttarget[ name ] = copy;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return the modified object\n\treturn target;\n};\n\njQuery.extend( {\n\n\t// Unique for each copy of jQuery on the page\n\texpando: \"jQuery\" + ( version + Math.random() ).replace( /\\D/g, \"\" ),\n\n\t// Assume jQuery is ready without the ready module\n\tisReady: true,\n\n\terror: function( msg ) {\n\t\tthrow new Error( msg );\n\t},\n\n\tnoop: function() {},\n\n\tisPlainObject: function( obj ) {\n\t\tvar proto, Ctor;\n\n\t\t// Detect obvious negatives\n\t\t// Use toString instead of jQuery.type to catch host objects\n\t\tif ( !obj || toString.call( obj ) !== \"[object Object]\" ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tproto = getProto( obj );\n\n\t\t// Objects with no prototype (e.g., `Object.create( null )`) are plain\n\t\tif ( !proto ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Objects with prototype are plain iff they were constructed by a global Object function\n\t\tCtor = hasOwn.call( proto, \"constructor\" ) && proto.constructor;\n\t\treturn typeof Ctor === \"function\" && fnToString.call( Ctor ) === ObjectFunctionString;\n\t},\n\n\tisEmptyObject: function( obj ) {\n\t\tvar name;\n\n\t\tfor ( name in obj ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t},\n\n\t// Evaluates a script in a provided context; falls back to the global one\n\t// if not specified.\n\tglobalEval: function( code, options, doc ) {\n\t\tDOMEval( code, { nonce: options && options.nonce }, doc );\n\t},\n\n\teach: function( obj, callback ) {\n\t\tvar length, i = 0;\n\n\t\tif ( isArrayLike( obj ) ) {\n\t\t\tlength = obj.length;\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tif ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor ( i in obj ) {\n\t\t\t\tif ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn obj;\n\t},\n\n\t// results is for internal usage only\n\tmakeArray: function( arr, results ) {\n\t\tvar ret = results || [];\n\n\t\tif ( arr != null ) {\n\t\t\tif ( isArrayLike( Object( arr ) ) ) {\n\t\t\t\tjQuery.merge( ret,\n\t\t\t\t\ttypeof arr === \"string\" ?\n\t\t\t\t\t\t[ arr ] : arr\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tpush.call( ret, arr );\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t},\n\n\tinArray: function( elem, arr, i ) {\n\t\treturn arr == null ? -1 : indexOf.call( arr, elem, i );\n\t},\n\n\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t// push.apply(_, arraylike) throws on ancient WebKit\n\tmerge: function( first, second ) {\n\t\tvar len = +second.length,\n\t\t\tj = 0,\n\t\t\ti = first.length;\n\n\t\tfor ( ; j < len; j++ ) {\n\t\t\tfirst[ i++ ] = second[ j ];\n\t\t}\n\n\t\tfirst.length = i;\n\n\t\treturn first;\n\t},\n\n\tgrep: function( elems, callback, invert ) {\n\t\tvar callbackInverse,\n\t\t\tmatches = [],\n\t\t\ti = 0,\n\t\t\tlength = elems.length,\n\t\t\tcallbackExpect = !invert;\n\n\t\t// Go through the array, only saving the items\n\t\t// that pass the validator function\n\t\tfor ( ; i < length; i++ ) {\n\t\t\tcallbackInverse = !callback( elems[ i ], i );\n\t\t\tif ( callbackInverse !== callbackExpect ) {\n\t\t\t\tmatches.push( elems[ i ] );\n\t\t\t}\n\t\t}\n\n\t\treturn matches;\n\t},\n\n\t// arg is for internal usage only\n\tmap: function( elems, callback, arg ) {\n\t\tvar length, value,\n\t\t\ti = 0,\n\t\t\tret = [];\n\n\t\t// Go through the array, translating each of the items to their new values\n\t\tif ( isArrayLike( elems ) ) {\n\t\t\tlength = elems.length;\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Go through every key on the object,\n\t\t} else {\n\t\t\tfor ( i in elems ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret.push( value );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Flatten any nested arrays\n\t\treturn flat( ret );\n\t},\n\n\t// A global GUID counter for objects\n\tguid: 1,\n\n\t// jQuery.support is not used in Core but other projects attach their\n\t// properties to it so it needs to exist.\n\tsupport: support\n} );\n\nif ( typeof Symbol === \"function\" ) {\n\tjQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ];\n}\n\n// Populate the class2type map\njQuery.each( \"Boolean Number String Function Array Date RegExp Object Error Symbol\".split( \" \" ),\n\tfunction( _i, name ) {\n\t\tclass2type[ \"[object \" + name + \"]\" ] = name.toLowerCase();\n\t} );\n\nfunction isArrayLike( obj ) {\n\n\t// Support: real iOS 8.2 only (not reproducible in simulator)\n\t// `in` check used to prevent JIT error (gh-2145)\n\t// hasOwn isn't used here due to false negatives\n\t// regarding Nodelist length in IE\n\tvar length = !!obj && \"length\" in obj && obj.length,\n\t\ttype = toType( obj );\n\n\tif ( isFunction( obj ) || isWindow( obj ) ) {\n\t\treturn false;\n\t}\n\n\treturn type === \"array\" || length === 0 ||\n\t\ttypeof length === \"number\" && length > 0 && ( length - 1 ) in obj;\n}\nvar Sizzle =\n/*!\n * Sizzle CSS Selector Engine v2.3.9\n * https://sizzlejs.com/\n *\n * Copyright JS Foundation and other contributors\n * Released under the MIT license\n * https://js.foundation/\n *\n * Date: 2022-12-19\n */\n( function( window ) {\nvar i,\n\tsupport,\n\tExpr,\n\tgetText,\n\tisXML,\n\ttokenize,\n\tcompile,\n\tselect,\n\toutermostContext,\n\tsortInput,\n\thasDuplicate,\n\n\t// Local document vars\n\tsetDocument,\n\tdocument,\n\tdocElem,\n\tdocumentIsHTML,\n\trbuggyQSA,\n\trbuggyMatches,\n\tmatches,\n\tcontains,\n\n\t// Instance-specific data\n\texpando = \"sizzle\" + 1 * new Date(),\n\tpreferredDoc = window.document,\n\tdirruns = 0,\n\tdone = 0,\n\tclassCache = createCache(),\n\ttokenCache = createCache(),\n\tcompilerCache = createCache(),\n\tnonnativeSelectorCache = createCache(),\n\tsortOrder = function( a, b ) {\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t}\n\t\treturn 0;\n\t},\n\n\t// Instance methods\n\thasOwn = ( {} ).hasOwnProperty,\n\tarr = [],\n\tpop = arr.pop,\n\tpushNative = arr.push,\n\tpush = arr.push,\n\tslice = arr.slice,\n\n\t// Use a stripped-down indexOf as it's faster than native\n\t// https://jsperf.com/thor-indexof-vs-for/5\n\tindexOf = function( list, elem ) {\n\t\tvar i = 0,\n\t\t\tlen = list.length;\n\t\tfor ( ; i < len; i++ ) {\n\t\t\tif ( list[ i ] === elem ) {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t\treturn -1;\n\t},\n\n\tbooleans = \"checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|\" +\n\t\t\"ismap|loop|multiple|open|readonly|required|scoped\",\n\n\t// Regular expressions\n\n\t// http://www.w3.org/TR/css3-selectors/#whitespace\n\twhitespace = \"[\\\\x20\\\\t\\\\r\\\\n\\\\f]\",\n\n\t// https://www.w3.org/TR/css-syntax-3/#ident-token-diagram\n\tidentifier = \"(?:\\\\\\\\[\\\\da-fA-F]{1,6}\" + whitespace +\n\t\t\"?|\\\\\\\\[^\\\\r\\\\n\\\\f]|[\\\\w-]|[^\\0-\\\\x7f])+\",\n\n\t// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors\n\tattributes = \"\\\\[\" + whitespace + \"*(\" + identifier + \")(?:\" + whitespace +\n\n\t\t// Operator (capture 2)\n\t\t\"*([*^$|!~]?=)\" + whitespace +\n\n\t\t// \"Attribute values must be CSS identifiers [capture 5]\n\t\t// or strings [capture 3 or capture 4]\"\n\t\t\"*(?:'((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\"|(\" + identifier + \"))|)\" +\n\t\twhitespace + \"*\\\\]\",\n\n\tpseudos = \":(\" + identifier + \")(?:\\\\((\" +\n\n\t\t// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:\n\t\t// 1. quoted (capture 3; capture 4 or capture 5)\n\t\t\"('((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\")|\" +\n\n\t\t// 2. simple (capture 6)\n\t\t\"((?:\\\\\\\\.|[^\\\\\\\\()[\\\\]]|\" + attributes + \")*)|\" +\n\n\t\t// 3. anything else (capture 2)\n\t\t\".*\" +\n\t\t\")\\\\)|)\",\n\n\t// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter\n\trwhitespace = new RegExp( whitespace + \"+\", \"g\" ),\n\trtrim = new RegExp( \"^\" + whitespace + \"+|((?:^|[^\\\\\\\\])(?:\\\\\\\\.)*)\" +\n\t\twhitespace + \"+$\", \"g\" ),\n\n\trcomma = new RegExp( \"^\" + whitespace + \"*,\" + whitespace + \"*\" ),\n\trcombinators = new RegExp( \"^\" + whitespace + \"*([>+~]|\" + whitespace + \")\" + whitespace +\n\t\t\"*\" ),\n\trdescend = new RegExp( whitespace + \"|>\" ),\n\n\trpseudo = new RegExp( pseudos ),\n\tridentifier = new RegExp( \"^\" + identifier + \"$\" ),\n\n\tmatchExpr = {\n\t\t\"ID\": new RegExp( \"^#(\" + identifier + \")\" ),\n\t\t\"CLASS\": new RegExp( \"^\\\\.(\" + identifier + \")\" ),\n\t\t\"TAG\": new RegExp( \"^(\" + identifier + \"|[*])\" ),\n\t\t\"ATTR\": new RegExp( \"^\" + attributes ),\n\t\t\"PSEUDO\": new RegExp( \"^\" + pseudos ),\n\t\t\"CHILD\": new RegExp( \"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\" +\n\t\t\twhitespace + \"*(even|odd|(([+-]|)(\\\\d*)n|)\" + whitespace + \"*(?:([+-]|)\" +\n\t\t\twhitespace + \"*(\\\\d+)|))\" + whitespace + \"*\\\\)|)\", \"i\" ),\n\t\t\"bool\": new RegExp( \"^(?:\" + booleans + \")$\", \"i\" ),\n\n\t\t// For use in libraries implementing .is()\n\t\t// We use this for POS matching in `select`\n\t\t\"needsContext\": new RegExp( \"^\" + whitespace +\n\t\t\t\"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\" + whitespace +\n\t\t\t\"*((?:-\\\\d)?\\\\d*)\" + whitespace + \"*\\\\)|)(?=[^-]|$)\", \"i\" )\n\t},\n\n\trhtml = /HTML$/i,\n\trinputs = /^(?:input|select|textarea|button)$/i,\n\trheader = /^h\\d$/i,\n\n\trnative = /^[^{]+\\{\\s*\\[native \\w/,\n\n\t// Easily-parseable/retrievable ID or TAG or CLASS selectors\n\trquickExpr = /^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,\n\n\trsibling = /[+~]/,\n\n\t// CSS escapes\n\t// http://www.w3.org/TR/CSS21/syndata.html#escaped-characters\n\trunescape = new RegExp( \"\\\\\\\\[\\\\da-fA-F]{1,6}\" + whitespace + \"?|\\\\\\\\([^\\\\r\\\\n\\\\f])\", \"g\" ),\n\tfunescape = function( escape, nonHex ) {\n\t\tvar high = \"0x\" + escape.slice( 1 ) - 0x10000;\n\n\t\treturn nonHex ?\n\n\t\t\t// Strip the backslash prefix from a non-hex escape sequence\n\t\t\tnonHex :\n\n\t\t\t// Replace a hexadecimal escape sequence with the encoded Unicode code point\n\t\t\t// Support: IE <=11+\n\t\t\t// For values outside the Basic Multilingual Plane (BMP), manually construct a\n\t\t\t// surrogate pair\n\t\t\thigh < 0 ?\n\t\t\t\tString.fromCharCode( high + 0x10000 ) :\n\t\t\t\tString.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );\n\t},\n\n\t// CSS string/identifier serialization\n\t// https://drafts.csswg.org/cssom/#common-serializing-idioms\n\trcssescape = /([\\0-\\x1f\\x7f]|^-?\\d)|^-$|[^\\0-\\x1f\\x7f-\\uFFFF\\w-]/g,\n\tfcssescape = function( ch, asCodePoint ) {\n\t\tif ( asCodePoint ) {\n\n\t\t\t// U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER\n\t\t\tif ( ch === \"\\0\" ) {\n\t\t\t\treturn \"\\uFFFD\";\n\t\t\t}\n\n\t\t\t// Control characters and (dependent upon position) numbers get escaped as code points\n\t\t\treturn ch.slice( 0, -1 ) + \"\\\\\" +\n\t\t\t\tch.charCodeAt( ch.length - 1 ).toString( 16 ) + \" \";\n\t\t}\n\n\t\t// Other potentially-special ASCII characters get backslash-escaped\n\t\treturn \"\\\\\" + ch;\n\t},\n\n\t// Used for iframes\n\t// See setDocument()\n\t// Removing the function wrapper causes a \"Permission Denied\"\n\t// error in IE\n\tunloadHandler = function() {\n\t\tsetDocument();\n\t},\n\n\tinDisabledFieldset = addCombinator(\n\t\tfunction( elem ) {\n\t\t\treturn elem.disabled === true && elem.nodeName.toLowerCase() === \"fieldset\";\n\t\t},\n\t\t{ dir: \"parentNode\", next: \"legend\" }\n\t);\n\n// Optimize for push.apply( _, NodeList )\ntry {\n\tpush.apply(\n\t\t( arr = slice.call( preferredDoc.childNodes ) ),\n\t\tpreferredDoc.childNodes\n\t);\n\n\t// Support: Android<4.0\n\t// Detect silently failing push.apply\n\t// eslint-disable-next-line no-unused-expressions\n\tarr[ preferredDoc.childNodes.length ].nodeType;\n} catch ( e ) {\n\tpush = { apply: arr.length ?\n\n\t\t// Leverage slice if possible\n\t\tfunction( target, els ) {\n\t\t\tpushNative.apply( target, slice.call( els ) );\n\t\t} :\n\n\t\t// Support: IE<9\n\t\t// Otherwise append directly\n\t\tfunction( target, els ) {\n\t\t\tvar j = target.length,\n\t\t\t\ti = 0;\n\n\t\t\t// Can't trust NodeList.length\n\t\t\twhile ( ( target[ j++ ] = els[ i++ ] ) ) {}\n\t\t\ttarget.length = j - 1;\n\t\t}\n\t};\n}\n\nfunction Sizzle( selector, context, results, seed ) {\n\tvar m, i, elem, nid, match, groups, newSelector,\n\t\tnewContext = context && context.ownerDocument,\n\n\t\t// nodeType defaults to 9, since context defaults to document\n\t\tnodeType = context ? context.nodeType : 9;\n\n\tresults = results || [];\n\n\t// Return early from calls with invalid selector or context\n\tif ( typeof selector !== \"string\" || !selector ||\n\t\tnodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {\n\n\t\treturn results;\n\t}\n\n\t// Try to shortcut find operations (as opposed to filters) in HTML documents\n\tif ( !seed ) {\n\t\tsetDocument( context );\n\t\tcontext = context || document;\n\n\t\tif ( documentIsHTML ) {\n\n\t\t\t// If the selector is sufficiently simple, try using a \"get*By*\" DOM method\n\t\t\t// (excepting DocumentFragment context, where the methods don't exist)\n\t\t\tif ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) {\n\n\t\t\t\t// ID selector\n\t\t\t\tif ( ( m = match[ 1 ] ) ) {\n\n\t\t\t\t\t// Document context\n\t\t\t\t\tif ( nodeType === 9 ) {\n\t\t\t\t\t\tif ( ( elem = context.getElementById( m ) ) ) {\n\n\t\t\t\t\t\t\t// Support: IE, Opera, Webkit\n\t\t\t\t\t\t\t// TODO: identify versions\n\t\t\t\t\t\t\t// getElementById can match elements by name instead of ID\n\t\t\t\t\t\t\tif ( elem.id === m ) {\n\t\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t// Element context\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// Support: IE, Opera, Webkit\n\t\t\t\t\t\t// TODO: identify versions\n\t\t\t\t\t\t// getElementById can match elements by name instead of ID\n\t\t\t\t\t\tif ( newContext && ( elem = newContext.getElementById( m ) ) &&\n\t\t\t\t\t\t\tcontains( context, elem ) &&\n\t\t\t\t\t\t\telem.id === m ) {\n\n\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\treturn results;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t// Type selector\n\t\t\t\t} else if ( match[ 2 ] ) {\n\t\t\t\t\tpush.apply( results, context.getElementsByTagName( selector ) );\n\t\t\t\t\treturn results;\n\n\t\t\t\t// Class selector\n\t\t\t\t} else if ( ( m = match[ 3 ] ) && support.getElementsByClassName &&\n\t\t\t\t\tcontext.getElementsByClassName ) {\n\n\t\t\t\t\tpush.apply( results, context.getElementsByClassName( m ) );\n\t\t\t\t\treturn results;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Take advantage of querySelectorAll\n\t\t\tif ( support.qsa &&\n\t\t\t\t!nonnativeSelectorCache[ selector + \" \" ] &&\n\t\t\t\t( !rbuggyQSA || !rbuggyQSA.test( selector ) ) &&\n\n\t\t\t\t// Support: IE 8 only\n\t\t\t\t// Exclude object elements\n\t\t\t\t( nodeType !== 1 || context.nodeName.toLowerCase() !== \"object\" ) ) {\n\n\t\t\t\tnewSelector = selector;\n\t\t\t\tnewContext = context;\n\n\t\t\t\t// qSA considers elements outside a scoping root when evaluating child or\n\t\t\t\t// descendant combinators, which is not what we want.\n\t\t\t\t// In such cases, we work around the behavior by prefixing every selector in the\n\t\t\t\t// list with an ID selector referencing the scope context.\n\t\t\t\t// The technique has to be used as well when a leading combinator is used\n\t\t\t\t// as such selectors are not recognized by querySelectorAll.\n\t\t\t\t// Thanks to Andrew Dupont for this technique.\n\t\t\t\tif ( nodeType === 1 &&\n\t\t\t\t\t( rdescend.test( selector ) || rcombinators.test( selector ) ) ) {\n\n\t\t\t\t\t// Expand context for sibling selectors\n\t\t\t\t\tnewContext = rsibling.test( selector ) && testContext( context.parentNode ) ||\n\t\t\t\t\t\tcontext;\n\n\t\t\t\t\t// We can use :scope instead of the ID hack if the browser\n\t\t\t\t\t// supports it & if we're not changing the context.\n\t\t\t\t\tif ( newContext !== context || !support.scope ) {\n\n\t\t\t\t\t\t// Capture the context ID, setting it first if necessary\n\t\t\t\t\t\tif ( ( nid = context.getAttribute( \"id\" ) ) ) {\n\t\t\t\t\t\t\tnid = nid.replace( rcssescape, fcssescape );\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcontext.setAttribute( \"id\", ( nid = expando ) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prefix every selector in the list\n\t\t\t\t\tgroups = tokenize( selector );\n\t\t\t\t\ti = groups.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tgroups[ i ] = ( nid ? \"#\" + nid : \":scope\" ) + \" \" +\n\t\t\t\t\t\t\ttoSelector( groups[ i ] );\n\t\t\t\t\t}\n\t\t\t\t\tnewSelector = groups.join( \",\" );\n\t\t\t\t}\n\n\t\t\t\ttry {\n\n\t\t\t\t\t// `qSA` may not throw for unrecognized parts using forgiving parsing:\n\t\t\t\t\t// https://drafts.csswg.org/selectors/#forgiving-selector\n\t\t\t\t\t// like the `:has()` pseudo-class:\n\t\t\t\t\t// https://drafts.csswg.org/selectors/#relational\n\t\t\t\t\t// `CSS.supports` is still expected to return `false` then:\n\t\t\t\t\t// https://drafts.csswg.org/css-conditional-4/#typedef-supports-selector-fn\n\t\t\t\t\t// https://drafts.csswg.org/css-conditional-4/#dfn-support-selector\n\t\t\t\t\tif ( support.cssSupportsSelector &&\n\n\t\t\t\t\t\t// eslint-disable-next-line no-undef\n\t\t\t\t\t\t!CSS.supports( \"selector(:is(\" + newSelector + \"))\" ) ) {\n\n\t\t\t\t\t\t// Support: IE 11+\n\t\t\t\t\t\t// Throw to get to the same code path as an error directly in qSA.\n\t\t\t\t\t\t// Note: once we only support browser supporting\n\t\t\t\t\t\t// `CSS.supports('selector(...)')`, we can most likely drop\n\t\t\t\t\t\t// the `try-catch`. IE doesn't implement the API.\n\t\t\t\t\t\tthrow new Error();\n\t\t\t\t\t}\n\n\t\t\t\t\tpush.apply( results,\n\t\t\t\t\t\tnewContext.querySelectorAll( newSelector )\n\t\t\t\t\t);\n\t\t\t\t\treturn results;\n\t\t\t\t} catch ( qsaError ) {\n\t\t\t\t\tnonnativeSelectorCache( selector, true );\n\t\t\t\t} finally {\n\t\t\t\t\tif ( nid === expando ) {\n\t\t\t\t\t\tcontext.removeAttribute( \"id\" );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// All others\n\treturn select( selector.replace( rtrim, \"$1\" ), context, results, seed );\n}\n\n/**\n * Create key-value caches of limited size\n * @returns {function(string, object)} Returns the Object data after storing it on itself with\n *\tproperty name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)\n *\tdeleting the oldest entry\n */\nfunction createCache() {\n\tvar keys = [];\n\n\tfunction cache( key, value ) {\n\n\t\t// Use (key + \" \") to avoid collision with native prototype properties (see Issue #157)\n\t\tif ( keys.push( key + \" \" ) > Expr.cacheLength ) {\n\n\t\t\t// Only keep the most recent entries\n\t\t\tdelete cache[ keys.shift() ];\n\t\t}\n\t\treturn ( cache[ key + \" \" ] = value );\n\t}\n\treturn cache;\n}\n\n/**\n * Mark a function for special use by Sizzle\n * @param {Function} fn The function to mark\n */\nfunction markFunction( fn ) {\n\tfn[ expando ] = true;\n\treturn fn;\n}\n\n/**\n * Support testing using an element\n * @param {Function} fn Passed the created element and returns a boolean result\n */\nfunction assert( fn ) {\n\tvar el = document.createElement( \"fieldset\" );\n\n\ttry {\n\t\treturn !!fn( el );\n\t} catch ( e ) {\n\t\treturn false;\n\t} finally {\n\n\t\t// Remove from its parent by default\n\t\tif ( el.parentNode ) {\n\t\t\tel.parentNode.removeChild( el );\n\t\t}\n\n\t\t// release memory in IE\n\t\tel = null;\n\t}\n}\n\n/**\n * Adds the same handler for all of the specified attrs\n * @param {String} attrs Pipe-separated list of attributes\n * @param {Function} handler The method that will be applied\n */\nfunction addHandle( attrs, handler ) {\n\tvar arr = attrs.split( \"|\" ),\n\t\ti = arr.length;\n\n\twhile ( i-- ) {\n\t\tExpr.attrHandle[ arr[ i ] ] = handler;\n\t}\n}\n\n/**\n * Checks document order of two siblings\n * @param {Element} a\n * @param {Element} b\n * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b\n */\nfunction siblingCheck( a, b ) {\n\tvar cur = b && a,\n\t\tdiff = cur && a.nodeType === 1 && b.nodeType === 1 &&\n\t\t\ta.sourceIndex - b.sourceIndex;\n\n\t// Use IE sourceIndex if available on both nodes\n\tif ( diff ) {\n\t\treturn diff;\n\t}\n\n\t// Check if b follows a\n\tif ( cur ) {\n\t\twhile ( ( cur = cur.nextSibling ) ) {\n\t\t\tif ( cur === b ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn a ? 1 : -1;\n}\n\n/**\n * Returns a function to use in pseudos for input types\n * @param {String} type\n */\nfunction createInputPseudo( type ) {\n\treturn function( elem ) {\n\t\tvar name = elem.nodeName.toLowerCase();\n\t\treturn name === \"input\" && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for buttons\n * @param {String} type\n */\nfunction createButtonPseudo( type ) {\n\treturn function( elem ) {\n\t\tvar name = elem.nodeName.toLowerCase();\n\t\treturn ( name === \"input\" || name === \"button\" ) && elem.type === type;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for :enabled/:disabled\n * @param {Boolean} disabled true for :disabled; false for :enabled\n */\nfunction createDisabledPseudo( disabled ) {\n\n\t// Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable\n\treturn function( elem ) {\n\n\t\t// Only certain elements can match :enabled or :disabled\n\t\t// https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled\n\t\t// https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled\n\t\tif ( \"form\" in elem ) {\n\n\t\t\t// Check for inherited disabledness on relevant non-disabled elements:\n\t\t\t// * listed form-associated elements in a disabled fieldset\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#category-listed\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled\n\t\t\t// * option elements in a disabled optgroup\n\t\t\t//   https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled\n\t\t\t// All such elements have a \"form\" property.\n\t\t\tif ( elem.parentNode && elem.disabled === false ) {\n\n\t\t\t\t// Option elements defer to a parent optgroup if present\n\t\t\t\tif ( \"label\" in elem ) {\n\t\t\t\t\tif ( \"label\" in elem.parentNode ) {\n\t\t\t\t\t\treturn elem.parentNode.disabled === disabled;\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn elem.disabled === disabled;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Support: IE 6 - 11\n\t\t\t\t// Use the isDisabled shortcut property to check for disabled fieldset ancestors\n\t\t\t\treturn elem.isDisabled === disabled ||\n\n\t\t\t\t\t// Where there is no isDisabled, check manually\n\t\t\t\t\t/* jshint -W018 */\n\t\t\t\t\telem.isDisabled !== !disabled &&\n\t\t\t\t\tinDisabledFieldset( elem ) === disabled;\n\t\t\t}\n\n\t\t\treturn elem.disabled === disabled;\n\n\t\t// Try to winnow out elements that can't be disabled before trusting the disabled property.\n\t\t// Some victims get caught in our net (label, legend, menu, track), but it shouldn't\n\t\t// even exist on them, let alone have a boolean value.\n\t\t} else if ( \"label\" in elem ) {\n\t\t\treturn elem.disabled === disabled;\n\t\t}\n\n\t\t// Remaining elements are neither :enabled nor :disabled\n\t\treturn false;\n\t};\n}\n\n/**\n * Returns a function to use in pseudos for positionals\n * @param {Function} fn\n */\nfunction createPositionalPseudo( fn ) {\n\treturn markFunction( function( argument ) {\n\t\targument = +argument;\n\t\treturn markFunction( function( seed, matches ) {\n\t\t\tvar j,\n\t\t\t\tmatchIndexes = fn( [], seed.length, argument ),\n\t\t\t\ti = matchIndexes.length;\n\n\t\t\t// Match elements found at the specified indexes\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( seed[ ( j = matchIndexes[ i ] ) ] ) {\n\t\t\t\t\tseed[ j ] = !( matches[ j ] = seed[ j ] );\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t} );\n}\n\n/**\n * Checks a node for validity as a Sizzle context\n * @param {Element|Object=} context\n * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value\n */\nfunction testContext( context ) {\n\treturn context && typeof context.getElementsByTagName !== \"undefined\" && context;\n}\n\n// Expose support vars for convenience\nsupport = Sizzle.support = {};\n\n/**\n * Detects XML nodes\n * @param {Element|Object} elem An element or a document\n * @returns {Boolean} True iff elem is a non-HTML XML node\n */\nisXML = Sizzle.isXML = function( elem ) {\n\tvar namespace = elem && elem.namespaceURI,\n\t\tdocElem = elem && ( elem.ownerDocument || elem ).documentElement;\n\n\t// Support: IE <=8\n\t// Assume HTML when documentElement doesn't yet exist, such as inside loading iframes\n\t// https://bugs.jquery.com/ticket/4833\n\treturn !rhtml.test( namespace || docElem && docElem.nodeName || \"HTML\" );\n};\n\n/**\n * Sets document-related variables once based on the current document\n * @param {Element|Object} [doc] An element or document object to use to set the document\n * @returns {Object} Returns the current document\n */\nsetDocument = Sizzle.setDocument = function( node ) {\n\tvar hasCompare, subWindow,\n\t\tdoc = node ? node.ownerDocument || node : preferredDoc;\n\n\t// Return early if doc is invalid or already selected\n\t// Support: IE 11+, Edge 17 - 18+\n\t// IE/Edge sometimes throw a \"Permission denied\" error when strict-comparing\n\t// two documents; shallow comparisons work.\n\t// eslint-disable-next-line eqeqeq\n\tif ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) {\n\t\treturn document;\n\t}\n\n\t// Update global variables\n\tdocument = doc;\n\tdocElem = document.documentElement;\n\tdocumentIsHTML = !isXML( document );\n\n\t// Support: IE 9 - 11+, Edge 12 - 18+\n\t// Accessing iframe documents after unload throws \"permission denied\" errors (jQuery #13936)\n\t// Support: IE 11+, Edge 17 - 18+\n\t// IE/Edge sometimes throw a \"Permission denied\" error when strict-comparing\n\t// two documents; shallow comparisons work.\n\t// eslint-disable-next-line eqeqeq\n\tif ( preferredDoc != document &&\n\t\t( subWindow = document.defaultView ) && subWindow.top !== subWindow ) {\n\n\t\t// Support: IE 11, Edge\n\t\tif ( subWindow.addEventListener ) {\n\t\t\tsubWindow.addEventListener( \"unload\", unloadHandler, false );\n\n\t\t// Support: IE 9 - 10 only\n\t\t} else if ( subWindow.attachEvent ) {\n\t\t\tsubWindow.attachEvent( \"onunload\", unloadHandler );\n\t\t}\n\t}\n\n\t// Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only,\n\t// Safari 4 - 5 only, Opera <=11.6 - 12.x only\n\t// IE/Edge & older browsers don't support the :scope pseudo-class.\n\t// Support: Safari 6.0 only\n\t// Safari 6.0 supports :scope but it's an alias of :root there.\n\tsupport.scope = assert( function( el ) {\n\t\tdocElem.appendChild( el ).appendChild( document.createElement( \"div\" ) );\n\t\treturn typeof el.querySelectorAll !== \"undefined\" &&\n\t\t\t!el.querySelectorAll( \":scope fieldset div\" ).length;\n\t} );\n\n\t// Support: Chrome 105+, Firefox 104+, Safari 15.4+\n\t// Make sure forgiving mode is not used in `CSS.supports( \"selector(...)\" )`.\n\t//\n\t// `:is()` uses a forgiving selector list as an argument and is widely\n\t// implemented, so it's a good one to test against.\n\tsupport.cssSupportsSelector = assert( function() {\n\t\t/* eslint-disable no-undef */\n\n\t\treturn CSS.supports( \"selector(*)\" ) &&\n\n\t\t\t// Support: Firefox 78-81 only\n\t\t\t// In old Firefox, `:is()` didn't use forgiving parsing. In that case,\n\t\t\t// fail this test as there's no selector to test against that.\n\t\t\t// `CSS.supports` uses unforgiving parsing\n\t\t\tdocument.querySelectorAll( \":is(:jqfake)\" ) &&\n\n\t\t\t// `*` is needed as Safari & newer Chrome implemented something in between\n\t\t\t// for `:has()` - it throws in `qSA` if it only contains an unsupported\n\t\t\t// argument but multiple ones, one of which is supported, are fine.\n\t\t\t// We want to play safe in case `:is()` gets the same treatment.\n\t\t\t!CSS.supports( \"selector(:is(*,:jqfake))\" );\n\n\t\t/* eslint-enable */\n\t} );\n\n\t/* Attributes\n\t---------------------------------------------------------------------- */\n\n\t// Support: IE<8\n\t// Verify that getAttribute really returns attributes and not properties\n\t// (excepting IE8 booleans)\n\tsupport.attributes = assert( function( el ) {\n\t\tel.className = \"i\";\n\t\treturn !el.getAttribute( \"className\" );\n\t} );\n\n\t/* getElement(s)By*\n\t---------------------------------------------------------------------- */\n\n\t// Check if getElementsByTagName(\"*\") returns only elements\n\tsupport.getElementsByTagName = assert( function( el ) {\n\t\tel.appendChild( document.createComment( \"\" ) );\n\t\treturn !el.getElementsByTagName( \"*\" ).length;\n\t} );\n\n\t// Support: IE<9\n\tsupport.getElementsByClassName = rnative.test( document.getElementsByClassName );\n\n\t// Support: IE<10\n\t// Check if getElementById returns elements by name\n\t// The broken getElementById methods don't pick up programmatically-set names,\n\t// so use a roundabout getElementsByName test\n\tsupport.getById = assert( function( el ) {\n\t\tdocElem.appendChild( el ).id = expando;\n\t\treturn !document.getElementsByName || !document.getElementsByName( expando ).length;\n\t} );\n\n\t// ID filter and find\n\tif ( support.getById ) {\n\t\tExpr.filter[ \"ID\" ] = function( id ) {\n\t\t\tvar attrId = id.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn elem.getAttribute( \"id\" ) === attrId;\n\t\t\t};\n\t\t};\n\t\tExpr.find[ \"ID\" ] = function( id, context ) {\n\t\t\tif ( typeof context.getElementById !== \"undefined\" && documentIsHTML ) {\n\t\t\t\tvar elem = context.getElementById( id );\n\t\t\t\treturn elem ? [ elem ] : [];\n\t\t\t}\n\t\t};\n\t} else {\n\t\tExpr.filter[ \"ID\" ] =  function( id ) {\n\t\t\tvar attrId = id.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\tvar node = typeof elem.getAttributeNode !== \"undefined\" &&\n\t\t\t\t\telem.getAttributeNode( \"id\" );\n\t\t\t\treturn node && node.value === attrId;\n\t\t\t};\n\t\t};\n\n\t\t// Support: IE 6 - 7 only\n\t\t// getElementById is not reliable as a find shortcut\n\t\tExpr.find[ \"ID\" ] = function( id, context ) {\n\t\t\tif ( typeof context.getElementById !== \"undefined\" && documentIsHTML ) {\n\t\t\t\tvar node, i, elems,\n\t\t\t\t\telem = context.getElementById( id );\n\n\t\t\t\tif ( elem ) {\n\n\t\t\t\t\t// Verify the id attribute\n\t\t\t\t\tnode = elem.getAttributeNode( \"id\" );\n\t\t\t\t\tif ( node && node.value === id ) {\n\t\t\t\t\t\treturn [ elem ];\n\t\t\t\t\t}\n\n\t\t\t\t\t// Fall back on getElementsByName\n\t\t\t\t\telems = context.getElementsByName( id );\n\t\t\t\t\ti = 0;\n\t\t\t\t\twhile ( ( elem = elems[ i++ ] ) ) {\n\t\t\t\t\t\tnode = elem.getAttributeNode( \"id\" );\n\t\t\t\t\t\tif ( node && node.value === id ) {\n\t\t\t\t\t\t\treturn [ elem ];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn [];\n\t\t\t}\n\t\t};\n\t}\n\n\t// Tag\n\tExpr.find[ \"TAG\" ] = support.getElementsByTagName ?\n\t\tfunction( tag, context ) {\n\t\t\tif ( typeof context.getElementsByTagName !== \"undefined\" ) {\n\t\t\t\treturn context.getElementsByTagName( tag );\n\n\t\t\t// DocumentFragment nodes don't have gEBTN\n\t\t\t} else if ( support.qsa ) {\n\t\t\t\treturn context.querySelectorAll( tag );\n\t\t\t}\n\t\t} :\n\n\t\tfunction( tag, context ) {\n\t\t\tvar elem,\n\t\t\t\ttmp = [],\n\t\t\t\ti = 0,\n\n\t\t\t\t// By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too\n\t\t\t\tresults = context.getElementsByTagName( tag );\n\n\t\t\t// Filter out possible comments\n\t\t\tif ( tag === \"*\" ) {\n\t\t\t\twhile ( ( elem = results[ i++ ] ) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\ttmp.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn tmp;\n\t\t\t}\n\t\t\treturn results;\n\t\t};\n\n\t// Class\n\tExpr.find[ \"CLASS\" ] = support.getElementsByClassName && function( className, context ) {\n\t\tif ( typeof context.getElementsByClassName !== \"undefined\" && documentIsHTML ) {\n\t\t\treturn context.getElementsByClassName( className );\n\t\t}\n\t};\n\n\t/* QSA/matchesSelector\n\t---------------------------------------------------------------------- */\n\n\t// QSA and matchesSelector support\n\n\t// matchesSelector(:active) reports false when true (IE9/Opera 11.5)\n\trbuggyMatches = [];\n\n\t// qSa(:focus) reports false when true (Chrome 21)\n\t// We allow this because of a bug in IE8/9 that throws an error\n\t// whenever `document.activeElement` is accessed on an iframe\n\t// So, we allow :focus to pass through QSA all the time to avoid the IE error\n\t// See https://bugs.jquery.com/ticket/13378\n\trbuggyQSA = [];\n\n\tif ( ( support.qsa = rnative.test( document.querySelectorAll ) ) ) {\n\n\t\t// Build QSA regex\n\t\t// Regex strategy adopted from Diego Perini\n\t\tassert( function( el ) {\n\n\t\t\tvar input;\n\n\t\t\t// Select is set to empty string on purpose\n\t\t\t// This is to test IE's treatment of not explicitly\n\t\t\t// setting a boolean content attribute,\n\t\t\t// since its presence should be enough\n\t\t\t// https://bugs.jquery.com/ticket/12359\n\t\t\tdocElem.appendChild( el ).innerHTML = \"<a id='\" + expando + \"'></a>\" +\n\t\t\t\t\"<select id='\" + expando + \"-\\r\\\\' msallowcapture=''>\" +\n\t\t\t\t\"<option selected=''></option></select>\";\n\n\t\t\t// Support: IE8, Opera 11-12.16\n\t\t\t// Nothing should be selected when empty strings follow ^= or $= or *=\n\t\t\t// The test attribute must be unknown in Opera but \"safe\" for WinRT\n\t\t\t// https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section\n\t\t\tif ( el.querySelectorAll( \"[msallowcapture^='']\" ).length ) {\n\t\t\t\trbuggyQSA.push( \"[*^$]=\" + whitespace + \"*(?:''|\\\"\\\")\" );\n\t\t\t}\n\n\t\t\t// Support: IE8\n\t\t\t// Boolean attributes and \"value\" are not treated correctly\n\t\t\tif ( !el.querySelectorAll( \"[selected]\" ).length ) {\n\t\t\t\trbuggyQSA.push( \"\\\\[\" + whitespace + \"*(?:value|\" + booleans + \")\" );\n\t\t\t}\n\n\t\t\t// Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+\n\t\t\tif ( !el.querySelectorAll( \"[id~=\" + expando + \"-]\" ).length ) {\n\t\t\t\trbuggyQSA.push( \"~=\" );\n\t\t\t}\n\n\t\t\t// Support: IE 11+, Edge 15 - 18+\n\t\t\t// IE 11/Edge don't find elements on a `[name='']` query in some cases.\n\t\t\t// Adding a temporary attribute to the document before the selection works\n\t\t\t// around the issue.\n\t\t\t// Interestingly, IE 10 & older don't seem to have the issue.\n\t\t\tinput = document.createElement( \"input\" );\n\t\t\tinput.setAttribute( \"name\", \"\" );\n\t\t\tel.appendChild( input );\n\t\t\tif ( !el.querySelectorAll( \"[name='']\" ).length ) {\n\t\t\t\trbuggyQSA.push( \"\\\\[\" + whitespace + \"*name\" + whitespace + \"*=\" +\n\t\t\t\t\twhitespace + \"*(?:''|\\\"\\\")\" );\n\t\t\t}\n\n\t\t\t// Webkit/Opera - :checked should return selected option elements\n\t\t\t// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\t// IE8 throws error here and will not see later tests\n\t\t\tif ( !el.querySelectorAll( \":checked\" ).length ) {\n\t\t\t\trbuggyQSA.push( \":checked\" );\n\t\t\t}\n\n\t\t\t// Support: Safari 8+, iOS 8+\n\t\t\t// https://bugs.webkit.org/show_bug.cgi?id=136851\n\t\t\t// In-page `selector#id sibling-combinator selector` fails\n\t\t\tif ( !el.querySelectorAll( \"a#\" + expando + \"+*\" ).length ) {\n\t\t\t\trbuggyQSA.push( \".#.+[+~]\" );\n\t\t\t}\n\n\t\t\t// Support: Firefox <=3.6 - 5 only\n\t\t\t// Old Firefox doesn't throw on a badly-escaped identifier.\n\t\t\t// el.querySelectorAll( \"\\\\\\f\" );\n\t\t\t// rbuggyQSA.push( \"[\\\\r\\\\n\\\\f]\" );\n\t\t} );\n\n\t\tassert( function( el ) {\n\t\t\tel.innerHTML = \"<a href='' disabled='disabled'></a>\" +\n\t\t\t\t\"<select disabled='disabled'><option/></select>\";\n\n\t\t\t// Support: Windows 8 Native Apps\n\t\t\t// The type and name attributes are restricted during .innerHTML assignment\n\t\t\tvar input = document.createElement( \"input\" );\n\t\t\tinput.setAttribute( \"type\", \"hidden\" );\n\t\t\tel.appendChild( input ).setAttribute( \"name\", \"D\" );\n\n\t\t\t// Support: IE8\n\t\t\t// Enforce case-sensitivity of name attribute\n\t\t\tif ( el.querySelectorAll( \"[name=d]\" ).length ) {\n\t\t\t\trbuggyQSA.push( \"name\" + whitespace + \"*[*^$|!~]?=\" );\n\t\t\t}\n\n\t\t\t// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)\n\t\t\t// IE8 throws error here and will not see later tests\n\t\t\tif ( el.querySelectorAll( \":enabled\" ).length !== 2 ) {\n\t\t\t\trbuggyQSA.push( \":enabled\", \":disabled\" );\n\t\t\t}\n\n\t\t\t// Support: IE9-11+\n\t\t\t// IE's :disabled selector does not pick up the children of disabled fieldsets\n\t\t\tdocElem.appendChild( el ).disabled = true;\n\t\t\tif ( el.querySelectorAll( \":disabled\" ).length !== 2 ) {\n\t\t\t\trbuggyQSA.push( \":enabled\", \":disabled\" );\n\t\t\t}\n\n\t\t\t// Support: Opera 10 - 11 only\n\t\t\t// Opera 10-11 does not throw on post-comma invalid pseudos\n\t\t\t// el.querySelectorAll( \"*,:x\" );\n\t\t\t// rbuggyQSA.push( \",.*:\" );\n\t\t} );\n\t}\n\n\tif ( ( support.matchesSelector = rnative.test( ( matches = docElem.matches ||\n\t\tdocElem.webkitMatchesSelector ||\n\t\tdocElem.mozMatchesSelector ||\n\t\tdocElem.oMatchesSelector ||\n\t\tdocElem.msMatchesSelector ) ) ) ) {\n\n\t\tassert( function( el ) {\n\n\t\t\t// Check to see if it's possible to do matchesSelector\n\t\t\t// on a disconnected node (IE 9)\n\t\t\tsupport.disconnectedMatch = matches.call( el, \"*\" );\n\n\t\t\t// This should fail with an exception\n\t\t\t// Gecko does not error, returns false instead\n\t\t\t// matches.call( el, \"[s!='']:x\" );\n\t\t\t// rbuggyMatches.push( \"!=\", pseudos );\n\t\t} );\n\t}\n\n\tif ( !support.cssSupportsSelector ) {\n\n\t\t// Support: Chrome 105+, Safari 15.4+\n\t\t// `:has()` uses a forgiving selector list as an argument so our regular\n\t\t// `try-catch` mechanism fails to catch `:has()` with arguments not supported\n\t\t// natively like `:has(:contains(\"Foo\"))`. Where supported & spec-compliant,\n\t\t// we now use `CSS.supports(\"selector(:is(SELECTOR_TO_BE_TESTED))\")`, but\n\t\t// outside that we mark `:has` as buggy.\n\t\trbuggyQSA.push( \":has\" );\n\t}\n\n\trbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( \"|\" ) );\n\trbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( \"|\" ) );\n\n\t/* Contains\n\t---------------------------------------------------------------------- */\n\thasCompare = rnative.test( docElem.compareDocumentPosition );\n\n\t// Element contains another\n\t// Purposefully self-exclusive\n\t// As in, an element does not contain itself\n\tcontains = hasCompare || rnative.test( docElem.contains ) ?\n\t\tfunction( a, b ) {\n\n\t\t\t// Support: IE <9 only\n\t\t\t// IE doesn't have `contains` on `document` so we need to check for\n\t\t\t// `documentElement` presence.\n\t\t\t// We need to fall back to `a` when `documentElement` is missing\n\t\t\t// as `ownerDocument` of elements within `<template/>` may have\n\t\t\t// a null one - a default behavior of all modern browsers.\n\t\t\tvar adown = a.nodeType === 9 && a.documentElement || a,\n\t\t\t\tbup = b && b.parentNode;\n\t\t\treturn a === bup || !!( bup && bup.nodeType === 1 && (\n\t\t\t\tadown.contains ?\n\t\t\t\t\tadown.contains( bup ) :\n\t\t\t\t\ta.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16\n\t\t\t) );\n\t\t} :\n\t\tfunction( a, b ) {\n\t\t\tif ( b ) {\n\t\t\t\twhile ( ( b = b.parentNode ) ) {\n\t\t\t\t\tif ( b === a ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n\n\t/* Sorting\n\t---------------------------------------------------------------------- */\n\n\t// Document order sorting\n\tsortOrder = hasCompare ?\n\tfunction( a, b ) {\n\n\t\t// Flag for duplicate removal\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\t// Sort on method existence if only one input has compareDocumentPosition\n\t\tvar compare = !a.compareDocumentPosition - !b.compareDocumentPosition;\n\t\tif ( compare ) {\n\t\t\treturn compare;\n\t\t}\n\n\t\t// Calculate position if both inputs belong to the same document\n\t\t// Support: IE 11+, Edge 17 - 18+\n\t\t// IE/Edge sometimes throw a \"Permission denied\" error when strict-comparing\n\t\t// two documents; shallow comparisons work.\n\t\t// eslint-disable-next-line eqeqeq\n\t\tcompare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ?\n\t\t\ta.compareDocumentPosition( b ) :\n\n\t\t\t// Otherwise we know they are disconnected\n\t\t\t1;\n\n\t\t// Disconnected nodes\n\t\tif ( compare & 1 ||\n\t\t\t( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) {\n\n\t\t\t// Choose the first element that is related to our preferred document\n\t\t\t// Support: IE 11+, Edge 17 - 18+\n\t\t\t// IE/Edge sometimes throw a \"Permission denied\" error when strict-comparing\n\t\t\t// two documents; shallow comparisons work.\n\t\t\t// eslint-disable-next-line eqeqeq\n\t\t\tif ( a == document || a.ownerDocument == preferredDoc &&\n\t\t\t\tcontains( preferredDoc, a ) ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\n\t\t\t// Support: IE 11+, Edge 17 - 18+\n\t\t\t// IE/Edge sometimes throw a \"Permission denied\" error when strict-comparing\n\t\t\t// two documents; shallow comparisons work.\n\t\t\t// eslint-disable-next-line eqeqeq\n\t\t\tif ( b == document || b.ownerDocument == preferredDoc &&\n\t\t\t\tcontains( preferredDoc, b ) ) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\t// Maintain original order\n\t\t\treturn sortInput ?\n\t\t\t\t( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :\n\t\t\t\t0;\n\t\t}\n\n\t\treturn compare & 4 ? -1 : 1;\n\t} :\n\tfunction( a, b ) {\n\n\t\t// Exit early if the nodes are identical\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\taup = a.parentNode,\n\t\t\tbup = b.parentNode,\n\t\t\tap = [ a ],\n\t\t\tbp = [ b ];\n\n\t\t// Parentless nodes are either documents or disconnected\n\t\tif ( !aup || !bup ) {\n\n\t\t\t// Support: IE 11+, Edge 17 - 18+\n\t\t\t// IE/Edge sometimes throw a \"Permission denied\" error when strict-comparing\n\t\t\t// two documents; shallow comparisons work.\n\t\t\t/* eslint-disable eqeqeq */\n\t\t\treturn a == document ? -1 :\n\t\t\t\tb == document ? 1 :\n\t\t\t\t/* eslint-enable eqeqeq */\n\t\t\t\taup ? -1 :\n\t\t\t\tbup ? 1 :\n\t\t\t\tsortInput ?\n\t\t\t\t( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :\n\t\t\t\t0;\n\n\t\t// If the nodes are siblings, we can do a quick check\n\t\t} else if ( aup === bup ) {\n\t\t\treturn siblingCheck( a, b );\n\t\t}\n\n\t\t// Otherwise we need full lists of their ancestors for comparison\n\t\tcur = a;\n\t\twhile ( ( cur = cur.parentNode ) ) {\n\t\t\tap.unshift( cur );\n\t\t}\n\t\tcur = b;\n\t\twhile ( ( cur = cur.parentNode ) ) {\n\t\t\tbp.unshift( cur );\n\t\t}\n\n\t\t// Walk down the tree looking for a discrepancy\n\t\twhile ( ap[ i ] === bp[ i ] ) {\n\t\t\ti++;\n\t\t}\n\n\t\treturn i ?\n\n\t\t\t// Do a sibling check if the nodes have a common ancestor\n\t\t\tsiblingCheck( ap[ i ], bp[ i ] ) :\n\n\t\t\t// Otherwise nodes in our document sort first\n\t\t\t// Support: IE 11+, Edge 17 - 18+\n\t\t\t// IE/Edge sometimes throw a \"Permission denied\" error when strict-comparing\n\t\t\t// two documents; shallow comparisons work.\n\t\t\t/* eslint-disable eqeqeq */\n\t\t\tap[ i ] == preferredDoc ? -1 :\n\t\t\tbp[ i ] == preferredDoc ? 1 :\n\t\t\t/* eslint-enable eqeqeq */\n\t\t\t0;\n\t};\n\n\treturn document;\n};\n\nSizzle.matches = function( expr, elements ) {\n\treturn Sizzle( expr, null, null, elements );\n};\n\nSizzle.matchesSelector = function( elem, expr ) {\n\tsetDocument( elem );\n\n\tif ( support.matchesSelector && documentIsHTML &&\n\t\t!nonnativeSelectorCache[ expr + \" \" ] &&\n\t\t( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&\n\t\t( !rbuggyQSA     || !rbuggyQSA.test( expr ) ) ) {\n\n\t\ttry {\n\t\t\tvar ret = matches.call( elem, expr );\n\n\t\t\t// IE 9's matchesSelector returns false on disconnected nodes\n\t\t\tif ( ret || support.disconnectedMatch ||\n\n\t\t\t\t// As well, disconnected nodes are said to be in a document\n\t\t\t\t// fragment in IE 9\n\t\t\t\telem.document && elem.document.nodeType !== 11 ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\t\t} catch ( e ) {\n\t\t\tnonnativeSelectorCache( expr, true );\n\t\t}\n\t}\n\n\treturn Sizzle( expr, document, null, [ elem ] ).length > 0;\n};\n\nSizzle.contains = function( context, elem ) {\n\n\t// Set document vars if needed\n\t// Support: IE 11+, Edge 17 - 18+\n\t// IE/Edge sometimes throw a \"Permission denied\" error when strict-comparing\n\t// two documents; shallow comparisons work.\n\t// eslint-disable-next-line eqeqeq\n\tif ( ( context.ownerDocument || context ) != document ) {\n\t\tsetDocument( context );\n\t}\n\treturn contains( context, elem );\n};\n\nSizzle.attr = function( elem, name ) {\n\n\t// Set document vars if needed\n\t// Support: IE 11+, Edge 17 - 18+\n\t// IE/Edge sometimes throw a \"Permission denied\" error when strict-comparing\n\t// two documents; shallow comparisons work.\n\t// eslint-disable-next-line eqeqeq\n\tif ( ( elem.ownerDocument || elem ) != document ) {\n\t\tsetDocument( elem );\n\t}\n\n\tvar fn = Expr.attrHandle[ name.toLowerCase() ],\n\n\t\t// Don't get fooled by Object.prototype properties (jQuery #13807)\n\t\tval = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?\n\t\t\tfn( elem, name, !documentIsHTML ) :\n\t\t\tundefined;\n\n\treturn val !== undefined ?\n\t\tval :\n\t\tsupport.attributes || !documentIsHTML ?\n\t\t\telem.getAttribute( name ) :\n\t\t\t( val = elem.getAttributeNode( name ) ) && val.specified ?\n\t\t\t\tval.value :\n\t\t\t\tnull;\n};\n\nSizzle.escape = function( sel ) {\n\treturn ( sel + \"\" ).replace( rcssescape, fcssescape );\n};\n\nSizzle.error = function( msg ) {\n\tthrow new Error( \"Syntax error, unrecognized expression: \" + msg );\n};\n\n/**\n * Document sorting and removing duplicates\n * @param {ArrayLike} results\n */\nSizzle.uniqueSort = function( results ) {\n\tvar elem,\n\t\tduplicates = [],\n\t\tj = 0,\n\t\ti = 0;\n\n\t// Unless we *know* we can detect duplicates, assume their presence\n\thasDuplicate = !support.detectDuplicates;\n\tsortInput = !support.sortStable && results.slice( 0 );\n\tresults.sort( sortOrder );\n\n\tif ( hasDuplicate ) {\n\t\twhile ( ( elem = results[ i++ ] ) ) {\n\t\t\tif ( elem === results[ i ] ) {\n\t\t\t\tj = duplicates.push( i );\n\t\t\t}\n\t\t}\n\t\twhile ( j-- ) {\n\t\t\tresults.splice( duplicates[ j ], 1 );\n\t\t}\n\t}\n\n\t// Clear input after sorting to release objects\n\t// See https://github.com/jquery/sizzle/pull/225\n\tsortInput = null;\n\n\treturn results;\n};\n\n/**\n * Utility function for retrieving the text value of an array of DOM nodes\n * @param {Array|Element} elem\n */\ngetText = Sizzle.getText = function( elem ) {\n\tvar node,\n\t\tret = \"\",\n\t\ti = 0,\n\t\tnodeType = elem.nodeType;\n\n\tif ( !nodeType ) {\n\n\t\t// If no nodeType, this is expected to be an array\n\t\twhile ( ( node = elem[ i++ ] ) ) {\n\n\t\t\t// Do not traverse comment nodes\n\t\t\tret += getText( node );\n\t\t}\n\t} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {\n\n\t\t// Use textContent for elements\n\t\t// innerText usage removed for consistency of new lines (jQuery #11153)\n\t\tif ( typeof elem.textContent === \"string\" ) {\n\t\t\treturn elem.textContent;\n\t\t} else {\n\n\t\t\t// Traverse its children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tret += getText( elem );\n\t\t\t}\n\t\t}\n\t} else if ( nodeType === 3 || nodeType === 4 ) {\n\t\treturn elem.nodeValue;\n\t}\n\n\t// Do not include comment or processing instruction nodes\n\n\treturn ret;\n};\n\nExpr = Sizzle.selectors = {\n\n\t// Can be adjusted by the user\n\tcacheLength: 50,\n\n\tcreatePseudo: markFunction,\n\n\tmatch: matchExpr,\n\n\tattrHandle: {},\n\n\tfind: {},\n\n\trelative: {\n\t\t\">\": { dir: \"parentNode\", first: true },\n\t\t\" \": { dir: \"parentNode\" },\n\t\t\"+\": { dir: \"previousSibling\", first: true },\n\t\t\"~\": { dir: \"previousSibling\" }\n\t},\n\n\tpreFilter: {\n\t\t\"ATTR\": function( match ) {\n\t\t\tmatch[ 1 ] = match[ 1 ].replace( runescape, funescape );\n\n\t\t\t// Move the given value to match[3] whether quoted or unquoted\n\t\t\tmatch[ 3 ] = ( match[ 3 ] || match[ 4 ] ||\n\t\t\t\tmatch[ 5 ] || \"\" ).replace( runescape, funescape );\n\n\t\t\tif ( match[ 2 ] === \"~=\" ) {\n\t\t\t\tmatch[ 3 ] = \" \" + match[ 3 ] + \" \";\n\t\t\t}\n\n\t\t\treturn match.slice( 0, 4 );\n\t\t},\n\n\t\t\"CHILD\": function( match ) {\n\n\t\t\t/* matches from matchExpr[\"CHILD\"]\n\t\t\t\t1 type (only|nth|...)\n\t\t\t\t2 what (child|of-type)\n\t\t\t\t3 argument (even|odd|\\d*|\\d*n([+-]\\d+)?|...)\n\t\t\t\t4 xn-component of xn+y argument ([+-]?\\d*n|)\n\t\t\t\t5 sign of xn-component\n\t\t\t\t6 x of xn-component\n\t\t\t\t7 sign of y-component\n\t\t\t\t8 y of y-component\n\t\t\t*/\n\t\t\tmatch[ 1 ] = match[ 1 ].toLowerCase();\n\n\t\t\tif ( match[ 1 ].slice( 0, 3 ) === \"nth\" ) {\n\n\t\t\t\t// nth-* requires argument\n\t\t\t\tif ( !match[ 3 ] ) {\n\t\t\t\t\tSizzle.error( match[ 0 ] );\n\t\t\t\t}\n\n\t\t\t\t// numeric x and y parameters for Expr.filter.CHILD\n\t\t\t\t// remember that false/true cast respectively to 0/1\n\t\t\t\tmatch[ 4 ] = +( match[ 4 ] ?\n\t\t\t\t\tmatch[ 5 ] + ( match[ 6 ] || 1 ) :\n\t\t\t\t\t2 * ( match[ 3 ] === \"even\" || match[ 3 ] === \"odd\" ) );\n\t\t\t\tmatch[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === \"odd\" );\n\n\t\t\t\t// other types prohibit arguments\n\t\t\t} else if ( match[ 3 ] ) {\n\t\t\t\tSizzle.error( match[ 0 ] );\n\t\t\t}\n\n\t\t\treturn match;\n\t\t},\n\n\t\t\"PSEUDO\": function( match ) {\n\t\t\tvar excess,\n\t\t\t\tunquoted = !match[ 6 ] && match[ 2 ];\n\n\t\t\tif ( matchExpr[ \"CHILD\" ].test( match[ 0 ] ) ) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Accept quoted arguments as-is\n\t\t\tif ( match[ 3 ] ) {\n\t\t\t\tmatch[ 2 ] = match[ 4 ] || match[ 5 ] || \"\";\n\n\t\t\t// Strip excess characters from unquoted arguments\n\t\t\t} else if ( unquoted && rpseudo.test( unquoted ) &&\n\n\t\t\t\t// Get excess from tokenize (recursively)\n\t\t\t\t( excess = tokenize( unquoted, true ) ) &&\n\n\t\t\t\t// advance to the next closing parenthesis\n\t\t\t\t( excess = unquoted.indexOf( \")\", unquoted.length - excess ) - unquoted.length ) ) {\n\n\t\t\t\t// excess is a negative index\n\t\t\t\tmatch[ 0 ] = match[ 0 ].slice( 0, excess );\n\t\t\t\tmatch[ 2 ] = unquoted.slice( 0, excess );\n\t\t\t}\n\n\t\t\t// Return only captures needed by the pseudo filter method (type and argument)\n\t\t\treturn match.slice( 0, 3 );\n\t\t}\n\t},\n\n\tfilter: {\n\n\t\t\"TAG\": function( nodeNameSelector ) {\n\t\t\tvar nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();\n\t\t\treturn nodeNameSelector === \"*\" ?\n\t\t\t\tfunction() {\n\t\t\t\t\treturn true;\n\t\t\t\t} :\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn elem.nodeName && elem.nodeName.toLowerCase() === nodeName;\n\t\t\t\t};\n\t\t},\n\n\t\t\"CLASS\": function( className ) {\n\t\t\tvar pattern = classCache[ className + \" \" ];\n\n\t\t\treturn pattern ||\n\t\t\t\t( pattern = new RegExp( \"(^|\" + whitespace +\n\t\t\t\t\t\")\" + className + \"(\" + whitespace + \"|$)\" ) ) && classCache(\n\t\t\t\t\t\tclassName, function( elem ) {\n\t\t\t\t\t\t\treturn pattern.test(\n\t\t\t\t\t\t\t\ttypeof elem.className === \"string\" && elem.className ||\n\t\t\t\t\t\t\t\ttypeof elem.getAttribute !== \"undefined\" &&\n\t\t\t\t\t\t\t\t\telem.getAttribute( \"class\" ) ||\n\t\t\t\t\t\t\t\t\"\"\n\t\t\t\t\t\t\t);\n\t\t\t\t} );\n\t\t},\n\n\t\t\"ATTR\": function( name, operator, check ) {\n\t\t\treturn function( elem ) {\n\t\t\t\tvar result = Sizzle.attr( elem, name );\n\n\t\t\t\tif ( result == null ) {\n\t\t\t\t\treturn operator === \"!=\";\n\t\t\t\t}\n\t\t\t\tif ( !operator ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tresult += \"\";\n\n\t\t\t\t/* eslint-disable max-len */\n\n\t\t\t\treturn operator === \"=\" ? result === check :\n\t\t\t\t\toperator === \"!=\" ? result !== check :\n\t\t\t\t\toperator === \"^=\" ? check && result.indexOf( check ) === 0 :\n\t\t\t\t\toperator === \"*=\" ? check && result.indexOf( check ) > -1 :\n\t\t\t\t\toperator === \"$=\" ? check && result.slice( -check.length ) === check :\n\t\t\t\t\toperator === \"~=\" ? ( \" \" + result.replace( rwhitespace, \" \" ) + \" \" ).indexOf( check ) > -1 :\n\t\t\t\t\toperator === \"|=\" ? result === check || result.slice( 0, check.length + 1 ) === check + \"-\" :\n\t\t\t\t\tfalse;\n\t\t\t\t/* eslint-enable max-len */\n\n\t\t\t};\n\t\t},\n\n\t\t\"CHILD\": function( type, what, _argument, first, last ) {\n\t\t\tvar simple = type.slice( 0, 3 ) !== \"nth\",\n\t\t\t\tforward = type.slice( -4 ) !== \"last\",\n\t\t\t\tofType = what === \"of-type\";\n\n\t\t\treturn first === 1 && last === 0 ?\n\n\t\t\t\t// Shortcut for :nth-*(n)\n\t\t\t\tfunction( elem ) {\n\t\t\t\t\treturn !!elem.parentNode;\n\t\t\t\t} :\n\n\t\t\t\tfunction( elem, _context, xml ) {\n\t\t\t\t\tvar cache, uniqueCache, outerCache, node, nodeIndex, start,\n\t\t\t\t\t\tdir = simple !== forward ? \"nextSibling\" : \"previousSibling\",\n\t\t\t\t\t\tparent = elem.parentNode,\n\t\t\t\t\t\tname = ofType && elem.nodeName.toLowerCase(),\n\t\t\t\t\t\tuseCache = !xml && !ofType,\n\t\t\t\t\t\tdiff = false;\n\n\t\t\t\t\tif ( parent ) {\n\n\t\t\t\t\t\t// :(first|last|only)-(child|of-type)\n\t\t\t\t\t\tif ( simple ) {\n\t\t\t\t\t\t\twhile ( dir ) {\n\t\t\t\t\t\t\t\tnode = elem;\n\t\t\t\t\t\t\t\twhile ( ( node = node[ dir ] ) ) {\n\t\t\t\t\t\t\t\t\tif ( ofType ?\n\t\t\t\t\t\t\t\t\t\tnode.nodeName.toLowerCase() === name :\n\t\t\t\t\t\t\t\t\t\tnode.nodeType === 1 ) {\n\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Reverse direction for :only-* (if we haven't yet done so)\n\t\t\t\t\t\t\t\tstart = dir = type === \"only\" && !start && \"nextSibling\";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tstart = [ forward ? parent.firstChild : parent.lastChild ];\n\n\t\t\t\t\t\t// non-xml :nth-child(...) stores cache data on `parent`\n\t\t\t\t\t\tif ( forward && useCache ) {\n\n\t\t\t\t\t\t\t// Seek `elem` from a previously-cached index\n\n\t\t\t\t\t\t\t// ...in a gzip-friendly way\n\t\t\t\t\t\t\tnode = parent;\n\t\t\t\t\t\t\touterCache = node[ expando ] || ( node[ expando ] = {} );\n\n\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t( outerCache[ node.uniqueID ] = {} );\n\n\t\t\t\t\t\t\tcache = uniqueCache[ type ] || [];\n\t\t\t\t\t\t\tnodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];\n\t\t\t\t\t\t\tdiff = nodeIndex && cache[ 2 ];\n\t\t\t\t\t\t\tnode = nodeIndex && parent.childNodes[ nodeIndex ];\n\n\t\t\t\t\t\t\twhile ( ( node = ++nodeIndex && node && node[ dir ] ||\n\n\t\t\t\t\t\t\t\t// Fallback to seeking `elem` from the start\n\t\t\t\t\t\t\t\t( diff = nodeIndex = 0 ) || start.pop() ) ) {\n\n\t\t\t\t\t\t\t\t// When found, cache indexes on `parent` and break\n\t\t\t\t\t\t\t\tif ( node.nodeType === 1 && ++diff && node === elem ) {\n\t\t\t\t\t\t\t\t\tuniqueCache[ type ] = [ dirruns, nodeIndex, diff ];\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// Use previously-cached element index if available\n\t\t\t\t\t\t\tif ( useCache ) {\n\n\t\t\t\t\t\t\t\t// ...in a gzip-friendly way\n\t\t\t\t\t\t\t\tnode = elem;\n\t\t\t\t\t\t\t\touterCache = node[ expando ] || ( node[ expando ] = {} );\n\n\t\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t\t( outerCache[ node.uniqueID ] = {} );\n\n\t\t\t\t\t\t\t\tcache = uniqueCache[ type ] || [];\n\t\t\t\t\t\t\t\tnodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];\n\t\t\t\t\t\t\t\tdiff = nodeIndex;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// xml :nth-child(...)\n\t\t\t\t\t\t\t// or :nth-last-child(...) or :nth(-last)?-of-type(...)\n\t\t\t\t\t\t\tif ( diff === false ) {\n\n\t\t\t\t\t\t\t\t// Use the same loop as above to seek `elem` from the start\n\t\t\t\t\t\t\t\twhile ( ( node = ++nodeIndex && node && node[ dir ] ||\n\t\t\t\t\t\t\t\t\t( diff = nodeIndex = 0 ) || start.pop() ) ) {\n\n\t\t\t\t\t\t\t\t\tif ( ( ofType ?\n\t\t\t\t\t\t\t\t\t\tnode.nodeName.toLowerCase() === name :\n\t\t\t\t\t\t\t\t\t\tnode.nodeType === 1 ) &&\n\t\t\t\t\t\t\t\t\t\t++diff ) {\n\n\t\t\t\t\t\t\t\t\t\t// Cache the index of each encountered element\n\t\t\t\t\t\t\t\t\t\tif ( useCache ) {\n\t\t\t\t\t\t\t\t\t\t\touterCache = node[ expando ] ||\n\t\t\t\t\t\t\t\t\t\t\t\t( node[ expando ] = {} );\n\n\t\t\t\t\t\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\t\t\t\t\t\tuniqueCache = outerCache[ node.uniqueID ] ||\n\t\t\t\t\t\t\t\t\t\t\t\t( outerCache[ node.uniqueID ] = {} );\n\n\t\t\t\t\t\t\t\t\t\t\tuniqueCache[ type ] = [ dirruns, diff ];\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\tif ( node === elem ) {\n\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Incorporate the offset, then check against cycle size\n\t\t\t\t\t\tdiff -= last;\n\t\t\t\t\t\treturn diff === first || ( diff % first === 0 && diff / first >= 0 );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t},\n\n\t\t\"PSEUDO\": function( pseudo, argument ) {\n\n\t\t\t// pseudo-class names are case-insensitive\n\t\t\t// http://www.w3.org/TR/selectors/#pseudo-classes\n\t\t\t// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters\n\t\t\t// Remember that setFilters inherits from pseudos\n\t\t\tvar args,\n\t\t\t\tfn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||\n\t\t\t\t\tSizzle.error( \"unsupported pseudo: \" + pseudo );\n\n\t\t\t// The user may use createPseudo to indicate that\n\t\t\t// arguments are needed to create the filter function\n\t\t\t// just as Sizzle does\n\t\t\tif ( fn[ expando ] ) {\n\t\t\t\treturn fn( argument );\n\t\t\t}\n\n\t\t\t// But maintain support for old signatures\n\t\t\tif ( fn.length > 1 ) {\n\t\t\t\targs = [ pseudo, pseudo, \"\", argument ];\n\t\t\t\treturn Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?\n\t\t\t\t\tmarkFunction( function( seed, matches ) {\n\t\t\t\t\t\tvar idx,\n\t\t\t\t\t\t\tmatched = fn( seed, argument ),\n\t\t\t\t\t\t\ti = matched.length;\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tidx = indexOf( seed, matched[ i ] );\n\t\t\t\t\t\t\tseed[ idx ] = !( matches[ idx ] = matched[ i ] );\n\t\t\t\t\t\t}\n\t\t\t\t\t} ) :\n\t\t\t\t\tfunction( elem ) {\n\t\t\t\t\t\treturn fn( elem, 0, args );\n\t\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn fn;\n\t\t}\n\t},\n\n\tpseudos: {\n\n\t\t// Potentially complex pseudos\n\t\t\"not\": markFunction( function( selector ) {\n\n\t\t\t// Trim the selector passed to compile\n\t\t\t// to avoid treating leading and trailing\n\t\t\t// spaces as combinators\n\t\t\tvar input = [],\n\t\t\t\tresults = [],\n\t\t\t\tmatcher = compile( selector.replace( rtrim, \"$1\" ) );\n\n\t\t\treturn matcher[ expando ] ?\n\t\t\t\tmarkFunction( function( seed, matches, _context, xml ) {\n\t\t\t\t\tvar elem,\n\t\t\t\t\t\tunmatched = matcher( seed, null, xml, [] ),\n\t\t\t\t\t\ti = seed.length;\n\n\t\t\t\t\t// Match elements unmatched by `matcher`\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( ( elem = unmatched[ i ] ) ) {\n\t\t\t\t\t\t\tseed[ i ] = !( matches[ i ] = elem );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} ) :\n\t\t\t\tfunction( elem, _context, xml ) {\n\t\t\t\t\tinput[ 0 ] = elem;\n\t\t\t\t\tmatcher( input, null, xml, results );\n\n\t\t\t\t\t// Don't keep the element (issue #299)\n\t\t\t\t\tinput[ 0 ] = null;\n\t\t\t\t\treturn !results.pop();\n\t\t\t\t};\n\t\t} ),\n\n\t\t\"has\": markFunction( function( selector ) {\n\t\t\treturn function( elem ) {\n\t\t\t\treturn Sizzle( selector, elem ).length > 0;\n\t\t\t};\n\t\t} ),\n\n\t\t\"contains\": markFunction( function( text ) {\n\t\t\ttext = text.replace( runescape, funescape );\n\t\t\treturn function( elem ) {\n\t\t\t\treturn ( elem.textContent || getText( elem ) ).indexOf( text ) > -1;\n\t\t\t};\n\t\t} ),\n\n\t\t// \"Whether an element is represented by a :lang() selector\n\t\t// is based solely on the element's language value\n\t\t// being equal to the identifier C,\n\t\t// or beginning with the identifier C immediately followed by \"-\".\n\t\t// The matching of C against the element's language value is performed case-insensitively.\n\t\t// The identifier C does not have to be a valid language name.\"\n\t\t// http://www.w3.org/TR/selectors/#lang-pseudo\n\t\t\"lang\": markFunction( function( lang ) {\n\n\t\t\t// lang value must be a valid identifier\n\t\t\tif ( !ridentifier.test( lang || \"\" ) ) {\n\t\t\t\tSizzle.error( \"unsupported lang: \" + lang );\n\t\t\t}\n\t\t\tlang = lang.replace( runescape, funescape ).toLowerCase();\n\t\t\treturn function( elem ) {\n\t\t\t\tvar elemLang;\n\t\t\t\tdo {\n\t\t\t\t\tif ( ( elemLang = documentIsHTML ?\n\t\t\t\t\t\telem.lang :\n\t\t\t\t\t\telem.getAttribute( \"xml:lang\" ) || elem.getAttribute( \"lang\" ) ) ) {\n\n\t\t\t\t\t\telemLang = elemLang.toLowerCase();\n\t\t\t\t\t\treturn elemLang === lang || elemLang.indexOf( lang + \"-\" ) === 0;\n\t\t\t\t\t}\n\t\t\t\t} while ( ( elem = elem.parentNode ) && elem.nodeType === 1 );\n\t\t\t\treturn false;\n\t\t\t};\n\t\t} ),\n\n\t\t// Miscellaneous\n\t\t\"target\": function( elem ) {\n\t\t\tvar hash = window.location && window.location.hash;\n\t\t\treturn hash && hash.slice( 1 ) === elem.id;\n\t\t},\n\n\t\t\"root\": function( elem ) {\n\t\t\treturn elem === docElem;\n\t\t},\n\n\t\t\"focus\": function( elem ) {\n\t\t\treturn elem === document.activeElement &&\n\t\t\t\t( !document.hasFocus || document.hasFocus() ) &&\n\t\t\t\t!!( elem.type || elem.href || ~elem.tabIndex );\n\t\t},\n\n\t\t// Boolean properties\n\t\t\"enabled\": createDisabledPseudo( false ),\n\t\t\"disabled\": createDisabledPseudo( true ),\n\n\t\t\"checked\": function( elem ) {\n\n\t\t\t// In CSS3, :checked should return both checked and selected elements\n\t\t\t// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n\t\t\tvar nodeName = elem.nodeName.toLowerCase();\n\t\t\treturn ( nodeName === \"input\" && !!elem.checked ) ||\n\t\t\t\t( nodeName === \"option\" && !!elem.selected );\n\t\t},\n\n\t\t\"selected\": function( elem ) {\n\n\t\t\t// Accessing this property makes selected-by-default\n\t\t\t// options in Safari work properly\n\t\t\tif ( elem.parentNode ) {\n\t\t\t\t// eslint-disable-next-line no-unused-expressions\n\t\t\t\telem.parentNode.selectedIndex;\n\t\t\t}\n\n\t\t\treturn elem.selected === true;\n\t\t},\n\n\t\t// Contents\n\t\t\"empty\": function( elem ) {\n\n\t\t\t// http://www.w3.org/TR/selectors/#empty-pseudo\n\t\t\t// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),\n\t\t\t//   but not by others (comment: 8; processing instruction: 7; etc.)\n\t\t\t// nodeType < 6 works because attributes (2) do not appear as children\n\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n\t\t\t\tif ( elem.nodeType < 6 ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\t\"parent\": function( elem ) {\n\t\t\treturn !Expr.pseudos[ \"empty\" ]( elem );\n\t\t},\n\n\t\t// Element/input types\n\t\t\"header\": function( elem ) {\n\t\t\treturn rheader.test( elem.nodeName );\n\t\t},\n\n\t\t\"input\": function( elem ) {\n\t\t\treturn rinputs.test( elem.nodeName );\n\t\t},\n\n\t\t\"button\": function( elem ) {\n\t\t\tvar name = elem.nodeName.toLowerCase();\n\t\t\treturn name === \"input\" && elem.type === \"button\" || name === \"button\";\n\t\t},\n\n\t\t\"text\": function( elem ) {\n\t\t\tvar attr;\n\t\t\treturn elem.nodeName.toLowerCase() === \"input\" &&\n\t\t\t\telem.type === \"text\" &&\n\n\t\t\t\t// Support: IE <10 only\n\t\t\t\t// New HTML5 attribute values (e.g., \"search\") appear with elem.type === \"text\"\n\t\t\t\t( ( attr = elem.getAttribute( \"type\" ) ) == null ||\n\t\t\t\t\tattr.toLowerCase() === \"text\" );\n\t\t},\n\n\t\t// Position-in-collection\n\t\t\"first\": createPositionalPseudo( function() {\n\t\t\treturn [ 0 ];\n\t\t} ),\n\n\t\t\"last\": createPositionalPseudo( function( _matchIndexes, length ) {\n\t\t\treturn [ length - 1 ];\n\t\t} ),\n\n\t\t\"eq\": createPositionalPseudo( function( _matchIndexes, length, argument ) {\n\t\t\treturn [ argument < 0 ? argument + length : argument ];\n\t\t} ),\n\n\t\t\"even\": createPositionalPseudo( function( matchIndexes, length ) {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t} ),\n\n\t\t\"odd\": createPositionalPseudo( function( matchIndexes, length ) {\n\t\t\tvar i = 1;\n\t\t\tfor ( ; i < length; i += 2 ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t} ),\n\n\t\t\"lt\": createPositionalPseudo( function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ?\n\t\t\t\targument + length :\n\t\t\t\targument > length ?\n\t\t\t\t\tlength :\n\t\t\t\t\targument;\n\t\t\tfor ( ; --i >= 0; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t} ),\n\n\t\t\"gt\": createPositionalPseudo( function( matchIndexes, length, argument ) {\n\t\t\tvar i = argument < 0 ? argument + length : argument;\n\t\t\tfor ( ; ++i < length; ) {\n\t\t\t\tmatchIndexes.push( i );\n\t\t\t}\n\t\t\treturn matchIndexes;\n\t\t} )\n\t}\n};\n\nExpr.pseudos[ \"nth\" ] = Expr.pseudos[ \"eq\" ];\n\n// Add button/input type pseudos\nfor ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {\n\tExpr.pseudos[ i ] = createInputPseudo( i );\n}\nfor ( i in { submit: true, reset: true } ) {\n\tExpr.pseudos[ i ] = createButtonPseudo( i );\n}\n\n// Easy API for creating new setFilters\nfunction setFilters() {}\nsetFilters.prototype = Expr.filters = Expr.pseudos;\nExpr.setFilters = new setFilters();\n\ntokenize = Sizzle.tokenize = function( selector, parseOnly ) {\n\tvar matched, match, tokens, type,\n\t\tsoFar, groups, preFilters,\n\t\tcached = tokenCache[ selector + \" \" ];\n\n\tif ( cached ) {\n\t\treturn parseOnly ? 0 : cached.slice( 0 );\n\t}\n\n\tsoFar = selector;\n\tgroups = [];\n\tpreFilters = Expr.preFilter;\n\n\twhile ( soFar ) {\n\n\t\t// Comma and first run\n\t\tif ( !matched || ( match = rcomma.exec( soFar ) ) ) {\n\t\t\tif ( match ) {\n\n\t\t\t\t// Don't consume trailing commas as valid\n\t\t\t\tsoFar = soFar.slice( match[ 0 ].length ) || soFar;\n\t\t\t}\n\t\t\tgroups.push( ( tokens = [] ) );\n\t\t}\n\n\t\tmatched = false;\n\n\t\t// Combinators\n\t\tif ( ( match = rcombinators.exec( soFar ) ) ) {\n\t\t\tmatched = match.shift();\n\t\t\ttokens.push( {\n\t\t\t\tvalue: matched,\n\n\t\t\t\t// Cast descendant combinators to space\n\t\t\t\ttype: match[ 0 ].replace( rtrim, \" \" )\n\t\t\t} );\n\t\t\tsoFar = soFar.slice( matched.length );\n\t\t}\n\n\t\t// Filters\n\t\tfor ( type in Expr.filter ) {\n\t\t\tif ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] ||\n\t\t\t\t( match = preFilters[ type ]( match ) ) ) ) {\n\t\t\t\tmatched = match.shift();\n\t\t\t\ttokens.push( {\n\t\t\t\t\tvalue: matched,\n\t\t\t\t\ttype: type,\n\t\t\t\t\tmatches: match\n\t\t\t\t} );\n\t\t\t\tsoFar = soFar.slice( matched.length );\n\t\t\t}\n\t\t}\n\n\t\tif ( !matched ) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Return the length of the invalid excess\n\t// if we're just parsing\n\t// Otherwise, throw an error or return tokens\n\treturn parseOnly ?\n\t\tsoFar.length :\n\t\tsoFar ?\n\t\t\tSizzle.error( selector ) :\n\n\t\t\t// Cache the tokens\n\t\t\ttokenCache( selector, groups ).slice( 0 );\n};\n\nfunction toSelector( tokens ) {\n\tvar i = 0,\n\t\tlen = tokens.length,\n\t\tselector = \"\";\n\tfor ( ; i < len; i++ ) {\n\t\tselector += tokens[ i ].value;\n\t}\n\treturn selector;\n}\n\nfunction addCombinator( matcher, combinator, base ) {\n\tvar dir = combinator.dir,\n\t\tskip = combinator.next,\n\t\tkey = skip || dir,\n\t\tcheckNonElements = base && key === \"parentNode\",\n\t\tdoneName = done++;\n\n\treturn combinator.first ?\n\n\t\t// Check against closest ancestor/preceding element\n\t\tfunction( elem, context, xml ) {\n\t\t\twhile ( ( elem = elem[ dir ] ) ) {\n\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\treturn matcher( elem, context, xml );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t} :\n\n\t\t// Check against all ancestor/preceding elements\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar oldCache, uniqueCache, outerCache,\n\t\t\t\tnewCache = [ dirruns, doneName ];\n\n\t\t\t// We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching\n\t\t\tif ( xml ) {\n\t\t\t\twhile ( ( elem = elem[ dir ] ) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\tif ( matcher( elem, context, xml ) ) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\twhile ( ( elem = elem[ dir ] ) ) {\n\t\t\t\t\tif ( elem.nodeType === 1 || checkNonElements ) {\n\t\t\t\t\t\touterCache = elem[ expando ] || ( elem[ expando ] = {} );\n\n\t\t\t\t\t\t// Support: IE <9 only\n\t\t\t\t\t\t// Defend against cloned attroperties (jQuery gh-1709)\n\t\t\t\t\t\tuniqueCache = outerCache[ elem.uniqueID ] ||\n\t\t\t\t\t\t\t( outerCache[ elem.uniqueID ] = {} );\n\n\t\t\t\t\t\tif ( skip && skip === elem.nodeName.toLowerCase() ) {\n\t\t\t\t\t\t\telem = elem[ dir ] || elem;\n\t\t\t\t\t\t} else if ( ( oldCache = uniqueCache[ key ] ) &&\n\t\t\t\t\t\t\toldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {\n\n\t\t\t\t\t\t\t// Assign to newCache so results back-propagate to previous elements\n\t\t\t\t\t\t\treturn ( newCache[ 2 ] = oldCache[ 2 ] );\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// Reuse newcache so results back-propagate to previous elements\n\t\t\t\t\t\t\tuniqueCache[ key ] = newCache;\n\n\t\t\t\t\t\t\t// A match means we're done; a fail means we have to keep checking\n\t\t\t\t\t\t\tif ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) {\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t};\n}\n\nfunction elementMatcher( matchers ) {\n\treturn matchers.length > 1 ?\n\t\tfunction( elem, context, xml ) {\n\t\t\tvar i = matchers.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( !matchers[ i ]( elem, context, xml ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t} :\n\t\tmatchers[ 0 ];\n}\n\nfunction multipleContexts( selector, contexts, results ) {\n\tvar i = 0,\n\t\tlen = contexts.length;\n\tfor ( ; i < len; i++ ) {\n\t\tSizzle( selector, contexts[ i ], results );\n\t}\n\treturn results;\n}\n\nfunction condense( unmatched, map, filter, context, xml ) {\n\tvar elem,\n\t\tnewUnmatched = [],\n\t\ti = 0,\n\t\tlen = unmatched.length,\n\t\tmapped = map != null;\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( ( elem = unmatched[ i ] ) ) {\n\t\t\tif ( !filter || filter( elem, context, xml ) ) {\n\t\t\t\tnewUnmatched.push( elem );\n\t\t\t\tif ( mapped ) {\n\t\t\t\t\tmap.push( i );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn newUnmatched;\n}\n\nfunction setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {\n\tif ( postFilter && !postFilter[ expando ] ) {\n\t\tpostFilter = setMatcher( postFilter );\n\t}\n\tif ( postFinder && !postFinder[ expando ] ) {\n\t\tpostFinder = setMatcher( postFinder, postSelector );\n\t}\n\treturn markFunction( function( seed, results, context, xml ) {\n\t\tvar temp, i, elem,\n\t\t\tpreMap = [],\n\t\t\tpostMap = [],\n\t\t\tpreexisting = results.length,\n\n\t\t\t// Get initial elements from seed or context\n\t\t\telems = seed || multipleContexts(\n\t\t\t\tselector || \"*\",\n\t\t\t\tcontext.nodeType ? [ context ] : context,\n\t\t\t\t[]\n\t\t\t),\n\n\t\t\t// Prefilter to get matcher input, preserving a map for seed-results synchronization\n\t\t\tmatcherIn = preFilter && ( seed || !selector ) ?\n\t\t\t\tcondense( elems, preMap, preFilter, context, xml ) :\n\t\t\t\telems,\n\n\t\t\tmatcherOut = matcher ?\n\n\t\t\t\t// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,\n\t\t\t\tpostFinder || ( seed ? preFilter : preexisting || postFilter ) ?\n\n\t\t\t\t\t// ...intermediate processing is necessary\n\t\t\t\t\t[] :\n\n\t\t\t\t\t// ...otherwise use results directly\n\t\t\t\t\tresults :\n\t\t\t\tmatcherIn;\n\n\t\t// Find primary matches\n\t\tif ( matcher ) {\n\t\t\tmatcher( matcherIn, matcherOut, context, xml );\n\t\t}\n\n\t\t// Apply postFilter\n\t\tif ( postFilter ) {\n\t\t\ttemp = condense( matcherOut, postMap );\n\t\t\tpostFilter( temp, [], context, xml );\n\n\t\t\t// Un-match failing elements by moving them back to matcherIn\n\t\t\ti = temp.length;\n\t\t\twhile ( i-- ) {\n\t\t\t\tif ( ( elem = temp[ i ] ) ) {\n\t\t\t\t\tmatcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( seed ) {\n\t\t\tif ( postFinder || preFilter ) {\n\t\t\t\tif ( postFinder ) {\n\n\t\t\t\t\t// Get the final matcherOut by condensing this intermediate into postFinder contexts\n\t\t\t\t\ttemp = [];\n\t\t\t\t\ti = matcherOut.length;\n\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\tif ( ( elem = matcherOut[ i ] ) ) {\n\n\t\t\t\t\t\t\t// Restore matcherIn since elem is not yet a final match\n\t\t\t\t\t\t\ttemp.push( ( matcherIn[ i ] = elem ) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpostFinder( null, ( matcherOut = [] ), temp, xml );\n\t\t\t\t}\n\n\t\t\t\t// Move matched elements from seed to results to keep them synchronized\n\t\t\t\ti = matcherOut.length;\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\tif ( ( elem = matcherOut[ i ] ) &&\n\t\t\t\t\t\t( temp = postFinder ? indexOf( seed, elem ) : preMap[ i ] ) > -1 ) {\n\n\t\t\t\t\t\tseed[ temp ] = !( results[ temp ] = elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Add elements to results, through postFinder if defined\n\t\t} else {\n\t\t\tmatcherOut = condense(\n\t\t\t\tmatcherOut === results ?\n\t\t\t\t\tmatcherOut.splice( preexisting, matcherOut.length ) :\n\t\t\t\t\tmatcherOut\n\t\t\t);\n\t\t\tif ( postFinder ) {\n\t\t\t\tpostFinder( null, results, matcherOut, xml );\n\t\t\t} else {\n\t\t\t\tpush.apply( results, matcherOut );\n\t\t\t}\n\t\t}\n\t} );\n}\n\nfunction matcherFromTokens( tokens ) {\n\tvar checkContext, matcher, j,\n\t\tlen = tokens.length,\n\t\tleadingRelative = Expr.relative[ tokens[ 0 ].type ],\n\t\timplicitRelative = leadingRelative || Expr.relative[ \" \" ],\n\t\ti = leadingRelative ? 1 : 0,\n\n\t\t// The foundational matcher ensures that elements are reachable from top-level context(s)\n\t\tmatchContext = addCombinator( function( elem ) {\n\t\t\treturn elem === checkContext;\n\t\t}, implicitRelative, true ),\n\t\tmatchAnyContext = addCombinator( function( elem ) {\n\t\t\treturn indexOf( checkContext, elem ) > -1;\n\t\t}, implicitRelative, true ),\n\t\tmatchers = [ function( elem, context, xml ) {\n\t\t\tvar ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (\n\t\t\t\t( checkContext = context ).nodeType ?\n\t\t\t\t\tmatchContext( elem, context, xml ) :\n\t\t\t\t\tmatchAnyContext( elem, context, xml ) );\n\n\t\t\t// Avoid hanging onto element (issue #299)\n\t\t\tcheckContext = null;\n\t\t\treturn ret;\n\t\t} ];\n\n\tfor ( ; i < len; i++ ) {\n\t\tif ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) {\n\t\t\tmatchers = [ addCombinator( elementMatcher( matchers ), matcher ) ];\n\t\t} else {\n\t\t\tmatcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches );\n\n\t\t\t// Return special upon seeing a positional matcher\n\t\t\tif ( matcher[ expando ] ) {\n\n\t\t\t\t// Find the next relative operator (if any) for proper handling\n\t\t\t\tj = ++i;\n\t\t\t\tfor ( ; j < len; j++ ) {\n\t\t\t\t\tif ( Expr.relative[ tokens[ j ].type ] ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn setMatcher(\n\t\t\t\t\ti > 1 && elementMatcher( matchers ),\n\t\t\t\t\ti > 1 && toSelector(\n\n\t\t\t\t\t// If the preceding token was a descendant combinator, insert an implicit any-element `*`\n\t\t\t\t\ttokens\n\t\t\t\t\t\t.slice( 0, i - 1 )\n\t\t\t\t\t\t.concat( { value: tokens[ i - 2 ].type === \" \" ? \"*\" : \"\" } )\n\t\t\t\t\t).replace( rtrim, \"$1\" ),\n\t\t\t\t\tmatcher,\n\t\t\t\t\ti < j && matcherFromTokens( tokens.slice( i, j ) ),\n\t\t\t\t\tj < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ),\n\t\t\t\t\tj < len && toSelector( tokens )\n\t\t\t\t);\n\t\t\t}\n\t\t\tmatchers.push( matcher );\n\t\t}\n\t}\n\n\treturn elementMatcher( matchers );\n}\n\nfunction matcherFromGroupMatchers( elementMatchers, setMatchers ) {\n\tvar bySet = setMatchers.length > 0,\n\t\tbyElement = elementMatchers.length > 0,\n\t\tsuperMatcher = function( seed, context, xml, results, outermost ) {\n\t\t\tvar elem, j, matcher,\n\t\t\t\tmatchedCount = 0,\n\t\t\t\ti = \"0\",\n\t\t\t\tunmatched = seed && [],\n\t\t\t\tsetMatched = [],\n\t\t\t\tcontextBackup = outermostContext,\n\n\t\t\t\t// We must always have either seed elements or outermost context\n\t\t\t\telems = seed || byElement && Expr.find[ \"TAG\" ]( \"*\", outermost ),\n\n\t\t\t\t// Use integer dirruns iff this is the outermost matcher\n\t\t\t\tdirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ),\n\t\t\t\tlen = elems.length;\n\n\t\t\tif ( outermost ) {\n\n\t\t\t\t// Support: IE 11+, Edge 17 - 18+\n\t\t\t\t// IE/Edge sometimes throw a \"Permission denied\" error when strict-comparing\n\t\t\t\t// two documents; shallow comparisons work.\n\t\t\t\t// eslint-disable-next-line eqeqeq\n\t\t\t\toutermostContext = context == document || context || outermost;\n\t\t\t}\n\n\t\t\t// Add elements passing elementMatchers directly to results\n\t\t\t// Support: IE<9, Safari\n\t\t\t// Tolerate NodeList properties (IE: \"length\"; Safari: <number>) matching elements by id\n\t\t\tfor ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) {\n\t\t\t\tif ( byElement && elem ) {\n\t\t\t\t\tj = 0;\n\n\t\t\t\t\t// Support: IE 11+, Edge 17 - 18+\n\t\t\t\t\t// IE/Edge sometimes throw a \"Permission denied\" error when strict-comparing\n\t\t\t\t\t// two documents; shallow comparisons work.\n\t\t\t\t\t// eslint-disable-next-line eqeqeq\n\t\t\t\t\tif ( !context && elem.ownerDocument != document ) {\n\t\t\t\t\t\tsetDocument( elem );\n\t\t\t\t\t\txml = !documentIsHTML;\n\t\t\t\t\t}\n\t\t\t\t\twhile ( ( matcher = elementMatchers[ j++ ] ) ) {\n\t\t\t\t\t\tif ( matcher( elem, context || document, xml ) ) {\n\t\t\t\t\t\t\tresults.push( elem );\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( outermost ) {\n\t\t\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Track unmatched elements for set filters\n\t\t\t\tif ( bySet ) {\n\n\t\t\t\t\t// They will have gone through all possible matchers\n\t\t\t\t\tif ( ( elem = !matcher && elem ) ) {\n\t\t\t\t\t\tmatchedCount--;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Lengthen the array for every element, matched or not\n\t\t\t\t\tif ( seed ) {\n\t\t\t\t\t\tunmatched.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// `i` is now the count of elements visited above, and adding it to `matchedCount`\n\t\t\t// makes the latter nonnegative.\n\t\t\tmatchedCount += i;\n\n\t\t\t// Apply set filters to unmatched elements\n\t\t\t// NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`\n\t\t\t// equals `i`), unless we didn't visit _any_ elements in the above loop because we have\n\t\t\t// no element matchers and no seed.\n\t\t\t// Incrementing an initially-string \"0\" `i` allows `i` to remain a string only in that\n\t\t\t// case, which will result in a \"00\" `matchedCount` that differs from `i` but is also\n\t\t\t// numerically zero.\n\t\t\tif ( bySet && i !== matchedCount ) {\n\t\t\t\tj = 0;\n\t\t\t\twhile ( ( matcher = setMatchers[ j++ ] ) ) {\n\t\t\t\t\tmatcher( unmatched, setMatched, context, xml );\n\t\t\t\t}\n\n\t\t\t\tif ( seed ) {\n\n\t\t\t\t\t// Reintegrate element matches to eliminate the need for sorting\n\t\t\t\t\tif ( matchedCount > 0 ) {\n\t\t\t\t\t\twhile ( i-- ) {\n\t\t\t\t\t\t\tif ( !( unmatched[ i ] || setMatched[ i ] ) ) {\n\t\t\t\t\t\t\t\tsetMatched[ i ] = pop.call( results );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Discard index placeholder values to get only actual matches\n\t\t\t\t\tsetMatched = condense( setMatched );\n\t\t\t\t}\n\n\t\t\t\t// Add matches to results\n\t\t\t\tpush.apply( results, setMatched );\n\n\t\t\t\t// Seedless set matches succeeding multiple successful matchers stipulate sorting\n\t\t\t\tif ( outermost && !seed && setMatched.length > 0 &&\n\t\t\t\t\t( matchedCount + setMatchers.length ) > 1 ) {\n\n\t\t\t\t\tSizzle.uniqueSort( results );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Override manipulation of globals by nested matchers\n\t\t\tif ( outermost ) {\n\t\t\t\tdirruns = dirrunsUnique;\n\t\t\t\toutermostContext = contextBackup;\n\t\t\t}\n\n\t\t\treturn unmatched;\n\t\t};\n\n\treturn bySet ?\n\t\tmarkFunction( superMatcher ) :\n\t\tsuperMatcher;\n}\n\ncompile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {\n\tvar i,\n\t\tsetMatchers = [],\n\t\telementMatchers = [],\n\t\tcached = compilerCache[ selector + \" \" ];\n\n\tif ( !cached ) {\n\n\t\t// Generate a function of recursive functions that can be used to check each element\n\t\tif ( !match ) {\n\t\t\tmatch = tokenize( selector );\n\t\t}\n\t\ti = match.length;\n\t\twhile ( i-- ) {\n\t\t\tcached = matcherFromTokens( match[ i ] );\n\t\t\tif ( cached[ expando ] ) {\n\t\t\t\tsetMatchers.push( cached );\n\t\t\t} else {\n\t\t\t\telementMatchers.push( cached );\n\t\t\t}\n\t\t}\n\n\t\t// Cache the compiled function\n\t\tcached = compilerCache(\n\t\t\tselector,\n\t\t\tmatcherFromGroupMatchers( elementMatchers, setMatchers )\n\t\t);\n\n\t\t// Save selector and tokenization\n\t\tcached.selector = selector;\n\t}\n\treturn cached;\n};\n\n/**\n * A low-level selection function that works with Sizzle's compiled\n *  selector functions\n * @param {String|Function} selector A selector or a pre-compiled\n *  selector function built with Sizzle.compile\n * @param {Element} context\n * @param {Array} [results]\n * @param {Array} [seed] A set of elements to match against\n */\nselect = Sizzle.select = function( selector, context, results, seed ) {\n\tvar i, tokens, token, type, find,\n\t\tcompiled = typeof selector === \"function\" && selector,\n\t\tmatch = !seed && tokenize( ( selector = compiled.selector || selector ) );\n\n\tresults = results || [];\n\n\t// Try to minimize operations if there is only one selector in the list and no seed\n\t// (the latter of which guarantees us context)\n\tif ( match.length === 1 ) {\n\n\t\t// Reduce context if the leading compound selector is an ID\n\t\ttokens = match[ 0 ] = match[ 0 ].slice( 0 );\n\t\tif ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === \"ID\" &&\n\t\t\tcontext.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) {\n\n\t\t\tcontext = ( Expr.find[ \"ID\" ]( token.matches[ 0 ]\n\t\t\t\t.replace( runescape, funescape ), context ) || [] )[ 0 ];\n\t\t\tif ( !context ) {\n\t\t\t\treturn results;\n\n\t\t\t// Precompiled matchers will still verify ancestry, so step up a level\n\t\t\t} else if ( compiled ) {\n\t\t\t\tcontext = context.parentNode;\n\t\t\t}\n\n\t\t\tselector = selector.slice( tokens.shift().value.length );\n\t\t}\n\n\t\t// Fetch a seed set for right-to-left matching\n\t\ti = matchExpr[ \"needsContext\" ].test( selector ) ? 0 : tokens.length;\n\t\twhile ( i-- ) {\n\t\t\ttoken = tokens[ i ];\n\n\t\t\t// Abort if we hit a combinator\n\t\t\tif ( Expr.relative[ ( type = token.type ) ] ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( ( find = Expr.find[ type ] ) ) {\n\n\t\t\t\t// Search, expanding context for leading sibling combinators\n\t\t\t\tif ( ( seed = find(\n\t\t\t\t\ttoken.matches[ 0 ].replace( runescape, funescape ),\n\t\t\t\t\trsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) ||\n\t\t\t\t\t\tcontext\n\t\t\t\t) ) ) {\n\n\t\t\t\t\t// If seed is empty or no tokens remain, we can return early\n\t\t\t\t\ttokens.splice( i, 1 );\n\t\t\t\t\tselector = seed.length && toSelector( tokens );\n\t\t\t\t\tif ( !selector ) {\n\t\t\t\t\t\tpush.apply( results, seed );\n\t\t\t\t\t\treturn results;\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Compile and execute a filtering function if one is not provided\n\t// Provide `match` to avoid retokenization if we modified the selector above\n\t( compiled || compile( selector, match ) )(\n\t\tseed,\n\t\tcontext,\n\t\t!documentIsHTML,\n\t\tresults,\n\t\t!context || rsibling.test( selector ) && testContext( context.parentNode ) || context\n\t);\n\treturn results;\n};\n\n// One-time assignments\n\n// Sort stability\nsupport.sortStable = expando.split( \"\" ).sort( sortOrder ).join( \"\" ) === expando;\n\n// Support: Chrome 14-35+\n// Always assume duplicates if they aren't passed to the comparison function\nsupport.detectDuplicates = !!hasDuplicate;\n\n// Initialize against the default document\nsetDocument();\n\n// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)\n// Detached nodes confoundingly follow *each other*\nsupport.sortDetached = assert( function( el ) {\n\n\t// Should return 1, but returns 4 (following)\n\treturn el.compareDocumentPosition( document.createElement( \"fieldset\" ) ) & 1;\n} );\n\n// Support: IE<8\n// Prevent attribute/property \"interpolation\"\n// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx\nif ( !assert( function( el ) {\n\tel.innerHTML = \"<a href='#'></a>\";\n\treturn el.firstChild.getAttribute( \"href\" ) === \"#\";\n} ) ) {\n\taddHandle( \"type|href|height|width\", function( elem, name, isXML ) {\n\t\tif ( !isXML ) {\n\t\t\treturn elem.getAttribute( name, name.toLowerCase() === \"type\" ? 1 : 2 );\n\t\t}\n\t} );\n}\n\n// Support: IE<9\n// Use defaultValue in place of getAttribute(\"value\")\nif ( !support.attributes || !assert( function( el ) {\n\tel.innerHTML = \"<input/>\";\n\tel.firstChild.setAttribute( \"value\", \"\" );\n\treturn el.firstChild.getAttribute( \"value\" ) === \"\";\n} ) ) {\n\taddHandle( \"value\", function( elem, _name, isXML ) {\n\t\tif ( !isXML && elem.nodeName.toLowerCase() === \"input\" ) {\n\t\t\treturn elem.defaultValue;\n\t\t}\n\t} );\n}\n\n// Support: IE<9\n// Use getAttributeNode to fetch booleans when getAttribute lies\nif ( !assert( function( el ) {\n\treturn el.getAttribute( \"disabled\" ) == null;\n} ) ) {\n\taddHandle( booleans, function( elem, name, isXML ) {\n\t\tvar val;\n\t\tif ( !isXML ) {\n\t\t\treturn elem[ name ] === true ? name.toLowerCase() :\n\t\t\t\t( val = elem.getAttributeNode( name ) ) && val.specified ?\n\t\t\t\t\tval.value :\n\t\t\t\t\tnull;\n\t\t}\n\t} );\n}\n\nreturn Sizzle;\n\n} )( window );\n\n\n\njQuery.find = Sizzle;\njQuery.expr = Sizzle.selectors;\n\n// Deprecated\njQuery.expr[ \":\" ] = jQuery.expr.pseudos;\njQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort;\njQuery.text = Sizzle.getText;\njQuery.isXMLDoc = Sizzle.isXML;\njQuery.contains = Sizzle.contains;\njQuery.escapeSelector = Sizzle.escape;\n\n\n\n\nvar dir = function( elem, dir, until ) {\n\tvar matched = [],\n\t\ttruncate = until !== undefined;\n\n\twhile ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {\n\t\tif ( elem.nodeType === 1 ) {\n\t\t\tif ( truncate && jQuery( elem ).is( until ) ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmatched.push( elem );\n\t\t}\n\t}\n\treturn matched;\n};\n\n\nvar siblings = function( n, elem ) {\n\tvar matched = [];\n\n\tfor ( ; n; n = n.nextSibling ) {\n\t\tif ( n.nodeType === 1 && n !== elem ) {\n\t\t\tmatched.push( n );\n\t\t}\n\t}\n\n\treturn matched;\n};\n\n\nvar rneedsContext = jQuery.expr.match.needsContext;\n\n\n\nfunction nodeName( elem, name ) {\n\n\treturn elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();\n\n}\nvar rsingleTag = ( /^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]*\\/?>(?:<\\/\\1>|)$/i );\n\n\n\n// Implement the identical functionality for filter and not\nfunction winnow( elements, qualifier, not ) {\n\tif ( isFunction( qualifier ) ) {\n\t\treturn jQuery.grep( elements, function( elem, i ) {\n\t\t\treturn !!qualifier.call( elem, i, elem ) !== not;\n\t\t} );\n\t}\n\n\t// Single element\n\tif ( qualifier.nodeType ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( elem === qualifier ) !== not;\n\t\t} );\n\t}\n\n\t// Arraylike of elements (jQuery, arguments, Array)\n\tif ( typeof qualifier !== \"string\" ) {\n\t\treturn jQuery.grep( elements, function( elem ) {\n\t\t\treturn ( indexOf.call( qualifier, elem ) > -1 ) !== not;\n\t\t} );\n\t}\n\n\t// Filtered directly for both simple and complex selectors\n\treturn jQuery.filter( qualifier, elements, not );\n}\n\njQuery.filter = function( expr, elems, not ) {\n\tvar elem = elems[ 0 ];\n\n\tif ( not ) {\n\t\texpr = \":not(\" + expr + \")\";\n\t}\n\n\tif ( elems.length === 1 && elem.nodeType === 1 ) {\n\t\treturn jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];\n\t}\n\n\treturn jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {\n\t\treturn elem.nodeType === 1;\n\t} ) );\n};\n\njQuery.fn.extend( {\n\tfind: function( selector ) {\n\t\tvar i, ret,\n\t\t\tlen = this.length,\n\t\t\tself = this;\n\n\t\tif ( typeof selector !== \"string\" ) {\n\t\t\treturn this.pushStack( jQuery( selector ).filter( function() {\n\t\t\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\t\t\tif ( jQuery.contains( self[ i ], this ) ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} ) );\n\t\t}\n\n\t\tret = this.pushStack( [] );\n\n\t\tfor ( i = 0; i < len; i++ ) {\n\t\t\tjQuery.find( selector, self[ i ], ret );\n\t\t}\n\n\t\treturn len > 1 ? jQuery.uniqueSort( ret ) : ret;\n\t},\n\tfilter: function( selector ) {\n\t\treturn this.pushStack( winnow( this, selector || [], false ) );\n\t},\n\tnot: function( selector ) {\n\t\treturn this.pushStack( winnow( this, selector || [], true ) );\n\t},\n\tis: function( selector ) {\n\t\treturn !!winnow(\n\t\t\tthis,\n\n\t\t\t// If this is a positional/relative selector, check membership in the returned set\n\t\t\t// so $(\"p:first\").is(\"p:last\") won't return true for a doc with two \"p\".\n\t\t\ttypeof selector === \"string\" && rneedsContext.test( selector ) ?\n\t\t\t\tjQuery( selector ) :\n\t\t\t\tselector || [],\n\t\t\tfalse\n\t\t).length;\n\t}\n} );\n\n\n// Initialize a jQuery object\n\n\n// A central reference to the root jQuery(document)\nvar rootjQuery,\n\n\t// A simple way to check for HTML strings\n\t// Prioritize #id over <tag> to avoid XSS via location.hash (trac-9521)\n\t// Strict HTML recognition (trac-11290: must start with <)\n\t// Shortcut simple #id case for speed\n\trquickExpr = /^(?:\\s*(<[\\w\\W]+>)[^>]*|#([\\w-]+))$/,\n\n\tinit = jQuery.fn.init = function( selector, context, root ) {\n\t\tvar match, elem;\n\n\t\t// HANDLE: $(\"\"), $(null), $(undefined), $(false)\n\t\tif ( !selector ) {\n\t\t\treturn this;\n\t\t}\n\n\t\t// Method init() accepts an alternate rootjQuery\n\t\t// so migrate can support jQuery.sub (gh-2101)\n\t\troot = root || rootjQuery;\n\n\t\t// Handle HTML strings\n\t\tif ( typeof selector === \"string\" ) {\n\t\t\tif ( selector[ 0 ] === \"<\" &&\n\t\t\t\tselector[ selector.length - 1 ] === \">\" &&\n\t\t\t\tselector.length >= 3 ) {\n\n\t\t\t\t// Assume that strings that start and end with <> are HTML and skip the regex check\n\t\t\t\tmatch = [ null, selector, null ];\n\n\t\t\t} else {\n\t\t\t\tmatch = rquickExpr.exec( selector );\n\t\t\t}\n\n\t\t\t// Match html or make sure no context is specified for #id\n\t\t\tif ( match && ( match[ 1 ] || !context ) ) {\n\n\t\t\t\t// HANDLE: $(html) -> $(array)\n\t\t\t\tif ( match[ 1 ] ) {\n\t\t\t\t\tcontext = context instanceof jQuery ? context[ 0 ] : context;\n\n\t\t\t\t\t// Option to run scripts is true for back-compat\n\t\t\t\t\t// Intentionally let the error be thrown if parseHTML is not present\n\t\t\t\t\tjQuery.merge( this, jQuery.parseHTML(\n\t\t\t\t\t\tmatch[ 1 ],\n\t\t\t\t\t\tcontext && context.nodeType ? context.ownerDocument || context : document,\n\t\t\t\t\t\ttrue\n\t\t\t\t\t) );\n\n\t\t\t\t\t// HANDLE: $(html, props)\n\t\t\t\t\tif ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {\n\t\t\t\t\t\tfor ( match in context ) {\n\n\t\t\t\t\t\t\t// Properties of context are called as methods if possible\n\t\t\t\t\t\t\tif ( isFunction( this[ match ] ) ) {\n\t\t\t\t\t\t\t\tthis[ match ]( context[ match ] );\n\n\t\t\t\t\t\t\t// ...and otherwise set as attributes\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tthis.attr( match, context[ match ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn this;\n\n\t\t\t\t// HANDLE: $(#id)\n\t\t\t\t} else {\n\t\t\t\t\telem = document.getElementById( match[ 2 ] );\n\n\t\t\t\t\tif ( elem ) {\n\n\t\t\t\t\t\t// Inject the element directly into the jQuery object\n\t\t\t\t\t\tthis[ 0 ] = elem;\n\t\t\t\t\t\tthis.length = 1;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\n\t\t\t// HANDLE: $(expr, $(...))\n\t\t\t} else if ( !context || context.jquery ) {\n\t\t\t\treturn ( context || root ).find( selector );\n\n\t\t\t// HANDLE: $(expr, context)\n\t\t\t// (which is just equivalent to: $(context).find(expr)\n\t\t\t} else {\n\t\t\t\treturn this.constructor( context ).find( selector );\n\t\t\t}\n\n\t\t// HANDLE: $(DOMElement)\n\t\t} else if ( selector.nodeType ) {\n\t\t\tthis[ 0 ] = selector;\n\t\t\tthis.length = 1;\n\t\t\treturn this;\n\n\t\t// HANDLE: $(function)\n\t\t// Shortcut for document ready\n\t\t} else if ( isFunction( selector ) ) {\n\t\t\treturn root.ready !== undefined ?\n\t\t\t\troot.ready( selector ) :\n\n\t\t\t\t// Execute immediately if ready is not present\n\t\t\t\tselector( jQuery );\n\t\t}\n\n\t\treturn jQuery.makeArray( selector, this );\n\t};\n\n// Give the init function the jQuery prototype for later instantiation\ninit.prototype = jQuery.fn;\n\n// Initialize central reference\nrootjQuery = jQuery( document );\n\n\nvar rparentsprev = /^(?:parents|prev(?:Until|All))/,\n\n\t// Methods guaranteed to produce a unique set when starting from a unique set\n\tguaranteedUnique = {\n\t\tchildren: true,\n\t\tcontents: true,\n\t\tnext: true,\n\t\tprev: true\n\t};\n\njQuery.fn.extend( {\n\thas: function( target ) {\n\t\tvar targets = jQuery( target, this ),\n\t\t\tl = targets.length;\n\n\t\treturn this.filter( function() {\n\t\t\tvar i = 0;\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tif ( jQuery.contains( this, targets[ i ] ) ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t},\n\n\tclosest: function( selectors, context ) {\n\t\tvar cur,\n\t\t\ti = 0,\n\t\t\tl = this.length,\n\t\t\tmatched = [],\n\t\t\ttargets = typeof selectors !== \"string\" && jQuery( selectors );\n\n\t\t// Positional selectors never match, since there's no _selection_ context\n\t\tif ( !rneedsContext.test( selectors ) ) {\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tfor ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) {\n\n\t\t\t\t\t// Always skip document fragments\n\t\t\t\t\tif ( cur.nodeType < 11 && ( targets ?\n\t\t\t\t\t\ttargets.index( cur ) > -1 :\n\n\t\t\t\t\t\t// Don't pass non-elements to Sizzle\n\t\t\t\t\t\tcur.nodeType === 1 &&\n\t\t\t\t\t\t\tjQuery.find.matchesSelector( cur, selectors ) ) ) {\n\n\t\t\t\t\t\tmatched.push( cur );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched );\n\t},\n\n\t// Determine the position of an element within the set\n\tindex: function( elem ) {\n\n\t\t// No argument, return index in parent\n\t\tif ( !elem ) {\n\t\t\treturn ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;\n\t\t}\n\n\t\t// Index in selector\n\t\tif ( typeof elem === \"string\" ) {\n\t\t\treturn indexOf.call( jQuery( elem ), this[ 0 ] );\n\t\t}\n\n\t\t// Locate the position of the desired element\n\t\treturn indexOf.call( this,\n\n\t\t\t// If it receives a jQuery object, the first element is used\n\t\t\telem.jquery ? elem[ 0 ] : elem\n\t\t);\n\t},\n\n\tadd: function( selector, context ) {\n\t\treturn this.pushStack(\n\t\t\tjQuery.uniqueSort(\n\t\t\t\tjQuery.merge( this.get(), jQuery( selector, context ) )\n\t\t\t)\n\t\t);\n\t},\n\n\taddBack: function( selector ) {\n\t\treturn this.add( selector == null ?\n\t\t\tthis.prevObject : this.prevObject.filter( selector )\n\t\t);\n\t}\n} );\n\nfunction sibling( cur, dir ) {\n\twhile ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {}\n\treturn cur;\n}\n\njQuery.each( {\n\tparent: function( elem ) {\n\t\tvar parent = elem.parentNode;\n\t\treturn parent && parent.nodeType !== 11 ? parent : null;\n\t},\n\tparents: function( elem ) {\n\t\treturn dir( elem, \"parentNode\" );\n\t},\n\tparentsUntil: function( elem, _i, until ) {\n\t\treturn dir( elem, \"parentNode\", until );\n\t},\n\tnext: function( elem ) {\n\t\treturn sibling( elem, \"nextSibling\" );\n\t},\n\tprev: function( elem ) {\n\t\treturn sibling( elem, \"previousSibling\" );\n\t},\n\tnextAll: function( elem ) {\n\t\treturn dir( elem, \"nextSibling\" );\n\t},\n\tprevAll: function( elem ) {\n\t\treturn dir( elem, \"previousSibling\" );\n\t},\n\tnextUntil: function( elem, _i, until ) {\n\t\treturn dir( elem, \"nextSibling\", until );\n\t},\n\tprevUntil: function( elem, _i, until ) {\n\t\treturn dir( elem, \"previousSibling\", until );\n\t},\n\tsiblings: function( elem ) {\n\t\treturn siblings( ( elem.parentNode || {} ).firstChild, elem );\n\t},\n\tchildren: function( elem ) {\n\t\treturn siblings( elem.firstChild );\n\t},\n\tcontents: function( elem ) {\n\t\tif ( elem.contentDocument != null &&\n\n\t\t\t// Support: IE 11+\n\t\t\t// <object> elements with no `data` attribute has an object\n\t\t\t// `contentDocument` with a `null` prototype.\n\t\t\tgetProto( elem.contentDocument ) ) {\n\n\t\t\treturn elem.contentDocument;\n\t\t}\n\n\t\t// Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only\n\t\t// Treat the template element as a regular one in browsers that\n\t\t// don't support it.\n\t\tif ( nodeName( elem, \"template\" ) ) {\n\t\t\telem = elem.content || elem;\n\t\t}\n\n\t\treturn jQuery.merge( [], elem.childNodes );\n\t}\n}, function( name, fn ) {\n\tjQuery.fn[ name ] = function( until, selector ) {\n\t\tvar matched = jQuery.map( this, fn, until );\n\n\t\tif ( name.slice( -5 ) !== \"Until\" ) {\n\t\t\tselector = until;\n\t\t}\n\n\t\tif ( selector && typeof selector === \"string\" ) {\n\t\t\tmatched = jQuery.filter( selector, matched );\n\t\t}\n\n\t\tif ( this.length > 1 ) {\n\n\t\t\t// Remove duplicates\n\t\t\tif ( !guaranteedUnique[ name ] ) {\n\t\t\t\tjQuery.uniqueSort( matched );\n\t\t\t}\n\n\t\t\t// Reverse order for parents* and prev-derivatives\n\t\t\tif ( rparentsprev.test( name ) ) {\n\t\t\t\tmatched.reverse();\n\t\t\t}\n\t\t}\n\n\t\treturn this.pushStack( matched );\n\t};\n} );\nvar rnothtmlwhite = ( /[^\\x20\\t\\r\\n\\f]+/g );\n\n\n\n// Convert String-formatted options into Object-formatted ones\nfunction createOptions( options ) {\n\tvar object = {};\n\tjQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) {\n\t\tobject[ flag ] = true;\n\t} );\n\treturn object;\n}\n\n/*\n * Create a callback list using the following parameters:\n *\n *\toptions: an optional list of space-separated options that will change how\n *\t\t\tthe callback list behaves or a more traditional option object\n *\n * By default a callback list will act like an event callback list and can be\n * \"fired\" multiple times.\n *\n * Possible options:\n *\n *\tonce:\t\t\twill ensure the callback list can only be fired once (like a Deferred)\n *\n *\tmemory:\t\t\twill keep track of previous values and will call any callback added\n *\t\t\t\t\tafter the list has been fired right away with the latest \"memorized\"\n *\t\t\t\t\tvalues (like a Deferred)\n *\n *\tunique:\t\t\twill ensure a callback can only be added once (no duplicate in the list)\n *\n *\tstopOnFalse:\tinterrupt callings when a callback returns false\n *\n */\njQuery.Callbacks = function( options ) {\n\n\t// Convert options from String-formatted to Object-formatted if needed\n\t// (we check in cache first)\n\toptions = typeof options === \"string\" ?\n\t\tcreateOptions( options ) :\n\t\tjQuery.extend( {}, options );\n\n\tvar // Flag to know if list is currently firing\n\t\tfiring,\n\n\t\t// Last fire value for non-forgettable lists\n\t\tmemory,\n\n\t\t// Flag to know if list was already fired\n\t\tfired,\n\n\t\t// Flag to prevent firing\n\t\tlocked,\n\n\t\t// Actual callback list\n\t\tlist = [],\n\n\t\t// Queue of execution data for repeatable lists\n\t\tqueue = [],\n\n\t\t// Index of currently firing callback (modified by add/remove as needed)\n\t\tfiringIndex = -1,\n\n\t\t// Fire callbacks\n\t\tfire = function() {\n\n\t\t\t// Enforce single-firing\n\t\t\tlocked = locked || options.once;\n\n\t\t\t// Execute callbacks for all pending executions,\n\t\t\t// respecting firingIndex overrides and runtime changes\n\t\t\tfired = firing = true;\n\t\t\tfor ( ; queue.length; firingIndex = -1 ) {\n\t\t\t\tmemory = queue.shift();\n\t\t\t\twhile ( ++firingIndex < list.length ) {\n\n\t\t\t\t\t// Run callback and check for early termination\n\t\t\t\t\tif ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&\n\t\t\t\t\t\toptions.stopOnFalse ) {\n\n\t\t\t\t\t\t// Jump to end and forget the data so .add doesn't re-fire\n\t\t\t\t\t\tfiringIndex = list.length;\n\t\t\t\t\t\tmemory = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Forget the data if we're done with it\n\t\t\tif ( !options.memory ) {\n\t\t\t\tmemory = false;\n\t\t\t}\n\n\t\t\tfiring = false;\n\n\t\t\t// Clean up if we're done firing for good\n\t\t\tif ( locked ) {\n\n\t\t\t\t// Keep an empty list if we have data for future add calls\n\t\t\t\tif ( memory ) {\n\t\t\t\t\tlist = [];\n\n\t\t\t\t// Otherwise, this object is spent\n\t\t\t\t} else {\n\t\t\t\t\tlist = \"\";\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t// Actual Callbacks object\n\t\tself = {\n\n\t\t\t// Add a callback or a collection of callbacks to the list\n\t\t\tadd: function() {\n\t\t\t\tif ( list ) {\n\n\t\t\t\t\t// If we have memory from a past run, we should fire after adding\n\t\t\t\t\tif ( memory && !firing ) {\n\t\t\t\t\t\tfiringIndex = list.length - 1;\n\t\t\t\t\t\tqueue.push( memory );\n\t\t\t\t\t}\n\n\t\t\t\t\t( function add( args ) {\n\t\t\t\t\t\tjQuery.each( args, function( _, arg ) {\n\t\t\t\t\t\t\tif ( isFunction( arg ) ) {\n\t\t\t\t\t\t\t\tif ( !options.unique || !self.has( arg ) ) {\n\t\t\t\t\t\t\t\t\tlist.push( arg );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if ( arg && arg.length && toType( arg ) !== \"string\" ) {\n\n\t\t\t\t\t\t\t\t// Inspect recursively\n\t\t\t\t\t\t\t\tadd( arg );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} );\n\t\t\t\t\t} )( arguments );\n\n\t\t\t\t\tif ( memory && !firing ) {\n\t\t\t\t\t\tfire();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Remove a callback from the list\n\t\t\tremove: function() {\n\t\t\t\tjQuery.each( arguments, function( _, arg ) {\n\t\t\t\t\tvar index;\n\t\t\t\t\twhile ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {\n\t\t\t\t\t\tlist.splice( index, 1 );\n\n\t\t\t\t\t\t// Handle firing indexes\n\t\t\t\t\t\tif ( index <= firingIndex ) {\n\t\t\t\t\t\t\tfiringIndex--;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Check if a given callback is in the list.\n\t\t\t// If no argument is given, return whether or not list has callbacks attached.\n\t\t\thas: function( fn ) {\n\t\t\t\treturn fn ?\n\t\t\t\t\tjQuery.inArray( fn, list ) > -1 :\n\t\t\t\t\tlist.length > 0;\n\t\t\t},\n\n\t\t\t// Remove all callbacks from the list\n\t\t\tempty: function() {\n\t\t\t\tif ( list ) {\n\t\t\t\t\tlist = [];\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Disable .fire and .add\n\t\t\t// Abort any current/pending executions\n\t\t\t// Clear all callbacks and values\n\t\t\tdisable: function() {\n\t\t\t\tlocked = queue = [];\n\t\t\t\tlist = memory = \"\";\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\tdisabled: function() {\n\t\t\t\treturn !list;\n\t\t\t},\n\n\t\t\t// Disable .fire\n\t\t\t// Also disable .add unless we have memory (since it would have no effect)\n\t\t\t// Abort any pending executions\n\t\t\tlock: function() {\n\t\t\t\tlocked = queue = [];\n\t\t\t\tif ( !memory && !firing ) {\n\t\t\t\t\tlist = memory = \"\";\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\tlocked: function() {\n\t\t\t\treturn !!locked;\n\t\t\t},\n\n\t\t\t// Call all callbacks with the given context and arguments\n\t\t\tfireWith: function( context, args ) {\n\t\t\t\tif ( !locked ) {\n\t\t\t\t\targs = args || [];\n\t\t\t\t\targs = [ context, args.slice ? args.slice() : args ];\n\t\t\t\t\tqueue.push( args );\n\t\t\t\t\tif ( !firing ) {\n\t\t\t\t\t\tfire();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// Call all the callbacks with the given arguments\n\t\t\tfire: function() {\n\t\t\t\tself.fireWith( this, arguments );\n\t\t\t\treturn this;\n\t\t\t},\n\n\t\t\t// To know if the callbacks have already been called at least once\n\t\t\tfired: function() {\n\t\t\t\treturn !!fired;\n\t\t\t}\n\t\t};\n\n\treturn self;\n};\n\n\nfunction Identity( v ) {\n\treturn v;\n}\nfunction Thrower( ex ) {\n\tthrow ex;\n}\n\nfunction adoptValue( value, resolve, reject, noValue ) {\n\tvar method;\n\n\ttry {\n\n\t\t// Check for promise aspect first to privilege synchronous behavior\n\t\tif ( value && isFunction( ( method = value.promise ) ) ) {\n\t\t\tmethod.call( value ).done( resolve ).fail( reject );\n\n\t\t// Other thenables\n\t\t} else if ( value && isFunction( ( method = value.then ) ) ) {\n\t\t\tmethod.call( value, resolve, reject );\n\n\t\t// Other non-thenables\n\t\t} else {\n\n\t\t\t// Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer:\n\t\t\t// * false: [ value ].slice( 0 ) => resolve( value )\n\t\t\t// * true: [ value ].slice( 1 ) => resolve()\n\t\t\tresolve.apply( undefined, [ value ].slice( noValue ) );\n\t\t}\n\n\t// For Promises/A+, convert exceptions into rejections\n\t// Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in\n\t// Deferred#then to conditionally suppress rejection.\n\t} catch ( value ) {\n\n\t\t// Support: Android 4.0 only\n\t\t// Strict mode functions invoked without .call/.apply get global-object context\n\t\treject.apply( undefined, [ value ] );\n\t}\n}\n\njQuery.extend( {\n\n\tDeferred: function( func ) {\n\t\tvar tuples = [\n\n\t\t\t\t// action, add listener, callbacks,\n\t\t\t\t// ... .then handlers, argument index, [final state]\n\t\t\t\t[ \"notify\", \"progress\", jQuery.Callbacks( \"memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"memory\" ), 2 ],\n\t\t\t\t[ \"resolve\", \"done\", jQuery.Callbacks( \"once memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"once memory\" ), 0, \"resolved\" ],\n\t\t\t\t[ \"reject\", \"fail\", jQuery.Callbacks( \"once memory\" ),\n\t\t\t\t\tjQuery.Callbacks( \"once memory\" ), 1, \"rejected\" ]\n\t\t\t],\n\t\t\tstate = \"pending\",\n\t\t\tpromise = {\n\t\t\t\tstate: function() {\n\t\t\t\t\treturn state;\n\t\t\t\t},\n\t\t\t\talways: function() {\n\t\t\t\t\tdeferred.done( arguments ).fail( arguments );\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\t\t\t\t\"catch\": function( fn ) {\n\t\t\t\t\treturn promise.then( null, fn );\n\t\t\t\t},\n\n\t\t\t\t// Keep pipe for back-compat\n\t\t\t\tpipe: function( /* fnDone, fnFail, fnProgress */ ) {\n\t\t\t\t\tvar fns = arguments;\n\n\t\t\t\t\treturn jQuery.Deferred( function( newDefer ) {\n\t\t\t\t\t\tjQuery.each( tuples, function( _i, tuple ) {\n\n\t\t\t\t\t\t\t// Map tuples (progress, done, fail) to arguments (done, fail, progress)\n\t\t\t\t\t\t\tvar fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ];\n\n\t\t\t\t\t\t\t// deferred.progress(function() { bind to newDefer or newDefer.notify })\n\t\t\t\t\t\t\t// deferred.done(function() { bind to newDefer or newDefer.resolve })\n\t\t\t\t\t\t\t// deferred.fail(function() { bind to newDefer or newDefer.reject })\n\t\t\t\t\t\t\tdeferred[ tuple[ 1 ] ]( function() {\n\t\t\t\t\t\t\t\tvar returned = fn && fn.apply( this, arguments );\n\t\t\t\t\t\t\t\tif ( returned && isFunction( returned.promise ) ) {\n\t\t\t\t\t\t\t\t\treturned.promise()\n\t\t\t\t\t\t\t\t\t\t.progress( newDefer.notify )\n\t\t\t\t\t\t\t\t\t\t.done( newDefer.resolve )\n\t\t\t\t\t\t\t\t\t\t.fail( newDefer.reject );\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tnewDefer[ tuple[ 0 ] + \"With\" ](\n\t\t\t\t\t\t\t\t\t\tthis,\n\t\t\t\t\t\t\t\t\t\tfn ? [ returned ] : arguments\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t} );\n\t\t\t\t\t\tfns = null;\n\t\t\t\t\t} ).promise();\n\t\t\t\t},\n\t\t\t\tthen: function( onFulfilled, onRejected, onProgress ) {\n\t\t\t\t\tvar maxDepth = 0;\n\t\t\t\t\tfunction resolve( depth, deferred, handler, special ) {\n\t\t\t\t\t\treturn function() {\n\t\t\t\t\t\t\tvar that = this,\n\t\t\t\t\t\t\t\targs = arguments,\n\t\t\t\t\t\t\t\tmightThrow = function() {\n\t\t\t\t\t\t\t\t\tvar returned, then;\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.3\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-59\n\t\t\t\t\t\t\t\t\t// Ignore double-resolution attempts\n\t\t\t\t\t\t\t\t\tif ( depth < maxDepth ) {\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\treturned = handler.apply( that, args );\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.1\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-48\n\t\t\t\t\t\t\t\t\tif ( returned === deferred.promise() ) {\n\t\t\t\t\t\t\t\t\t\tthrow new TypeError( \"Thenable self-resolution\" );\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// Support: Promises/A+ sections 2.3.3.1, 3.5\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-54\n\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-75\n\t\t\t\t\t\t\t\t\t// Retrieve `then` only once\n\t\t\t\t\t\t\t\t\tthen = returned &&\n\n\t\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.4\n\t\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-64\n\t\t\t\t\t\t\t\t\t\t// Only check objects and functions for thenability\n\t\t\t\t\t\t\t\t\t\t( typeof returned === \"object\" ||\n\t\t\t\t\t\t\t\t\t\t\ttypeof returned === \"function\" ) &&\n\t\t\t\t\t\t\t\t\t\treturned.then;\n\n\t\t\t\t\t\t\t\t\t// Handle a returned thenable\n\t\t\t\t\t\t\t\t\tif ( isFunction( then ) ) {\n\n\t\t\t\t\t\t\t\t\t\t// Special processors (notify) just wait for resolution\n\t\t\t\t\t\t\t\t\t\tif ( special ) {\n\t\t\t\t\t\t\t\t\t\t\tthen.call(\n\t\t\t\t\t\t\t\t\t\t\t\treturned,\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Thrower, special )\n\t\t\t\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\t\t\t// Normal processors (resolve) also hook into progress\n\t\t\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\t\t\t// ...and disregard older resolution values\n\t\t\t\t\t\t\t\t\t\t\tmaxDepth++;\n\n\t\t\t\t\t\t\t\t\t\t\tthen.call(\n\t\t\t\t\t\t\t\t\t\t\t\treturned,\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Thrower, special ),\n\t\t\t\t\t\t\t\t\t\t\t\tresolve( maxDepth, deferred, Identity,\n\t\t\t\t\t\t\t\t\t\t\t\t\tdeferred.notifyWith )\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// Handle all other returned values\n\t\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\t\t// Only substitute handlers pass on context\n\t\t\t\t\t\t\t\t\t\t// and multiple values (non-spec behavior)\n\t\t\t\t\t\t\t\t\t\tif ( handler !== Identity ) {\n\t\t\t\t\t\t\t\t\t\t\tthat = undefined;\n\t\t\t\t\t\t\t\t\t\t\targs = [ returned ];\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t// Process the value(s)\n\t\t\t\t\t\t\t\t\t\t// Default process is resolve\n\t\t\t\t\t\t\t\t\t\t( special || deferred.resolveWith )( that, args );\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\n\t\t\t\t\t\t\t\t// Only normal processors (resolve) catch and reject exceptions\n\t\t\t\t\t\t\t\tprocess = special ?\n\t\t\t\t\t\t\t\t\tmightThrow :\n\t\t\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\t\tmightThrow();\n\t\t\t\t\t\t\t\t\t\t} catch ( e ) {\n\n\t\t\t\t\t\t\t\t\t\t\tif ( jQuery.Deferred.exceptionHook ) {\n\t\t\t\t\t\t\t\t\t\t\t\tjQuery.Deferred.exceptionHook( e,\n\t\t\t\t\t\t\t\t\t\t\t\t\tprocess.stackTrace );\n\t\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.4.1\n\t\t\t\t\t\t\t\t\t\t\t// https://promisesaplus.com/#point-61\n\t\t\t\t\t\t\t\t\t\t\t// Ignore post-resolution exceptions\n\t\t\t\t\t\t\t\t\t\t\tif ( depth + 1 >= maxDepth ) {\n\n\t\t\t\t\t\t\t\t\t\t\t\t// Only substitute handlers pass on context\n\t\t\t\t\t\t\t\t\t\t\t\t// and multiple values (non-spec behavior)\n\t\t\t\t\t\t\t\t\t\t\t\tif ( handler !== Thrower ) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tthat = undefined;\n\t\t\t\t\t\t\t\t\t\t\t\t\targs = [ e ];\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t\t\tdeferred.rejectWith( that, args );\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t// Support: Promises/A+ section 2.3.3.3.1\n\t\t\t\t\t\t\t// https://promisesaplus.com/#point-57\n\t\t\t\t\t\t\t// Re-resolve promises immediately to dodge false rejection from\n\t\t\t\t\t\t\t// subsequent errors\n\t\t\t\t\t\t\tif ( depth ) {\n\t\t\t\t\t\t\t\tprocess();\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t// Call an optional hook to record the stack, in case of exception\n\t\t\t\t\t\t\t\t// since it's otherwise lost when execution goes async\n\t\t\t\t\t\t\t\tif ( jQuery.Deferred.getStackHook ) {\n\t\t\t\t\t\t\t\t\tprocess.stackTrace = jQuery.Deferred.getStackHook();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\twindow.setTimeout( process );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\treturn jQuery.Deferred( function( newDefer ) {\n\n\t\t\t\t\t\t// progress_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 0 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tisFunction( onProgress ) ?\n\t\t\t\t\t\t\t\t\tonProgress :\n\t\t\t\t\t\t\t\t\tIdentity,\n\t\t\t\t\t\t\t\tnewDefer.notifyWith\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// fulfilled_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 1 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tisFunction( onFulfilled ) ?\n\t\t\t\t\t\t\t\t\tonFulfilled :\n\t\t\t\t\t\t\t\t\tIdentity\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// rejected_handlers.add( ... )\n\t\t\t\t\t\ttuples[ 2 ][ 3 ].add(\n\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\tnewDefer,\n\t\t\t\t\t\t\t\tisFunction( onRejected ) ?\n\t\t\t\t\t\t\t\t\tonRejected :\n\t\t\t\t\t\t\t\t\tThrower\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\t\t\t\t\t} ).promise();\n\t\t\t\t},\n\n\t\t\t\t// Get a promise for this deferred\n\t\t\t\t// If obj is provided, the promise aspect is added to the object\n\t\t\t\tpromise: function( obj ) {\n\t\t\t\t\treturn obj != null ? jQuery.extend( obj, promise ) : promise;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdeferred = {};\n\n\t\t// Add list-specific methods\n\t\tjQuery.each( tuples, function( i, tuple ) {\n\t\t\tvar list = tuple[ 2 ],\n\t\t\t\tstateString = tuple[ 5 ];\n\n\t\t\t// promise.progress = list.add\n\t\t\t// promise.done = list.add\n\t\t\t// promise.fail = list.add\n\t\t\tpromise[ tuple[ 1 ] ] = list.add;\n\n\t\t\t// Handle state\n\t\t\tif ( stateString ) {\n\t\t\t\tlist.add(\n\t\t\t\t\tfunction() {\n\n\t\t\t\t\t\t// state = \"resolved\" (i.e., fulfilled)\n\t\t\t\t\t\t// state = \"rejected\"\n\t\t\t\t\t\tstate = stateString;\n\t\t\t\t\t},\n\n\t\t\t\t\t// rejected_callbacks.disable\n\t\t\t\t\t// fulfilled_callbacks.disable\n\t\t\t\t\ttuples[ 3 - i ][ 2 ].disable,\n\n\t\t\t\t\t// rejected_handlers.disable\n\t\t\t\t\t// fulfilled_handlers.disable\n\t\t\t\t\ttuples[ 3 - i ][ 3 ].disable,\n\n\t\t\t\t\t// progress_callbacks.lock\n\t\t\t\t\ttuples[ 0 ][ 2 ].lock,\n\n\t\t\t\t\t// progress_handlers.lock\n\t\t\t\t\ttuples[ 0 ][ 3 ].lock\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// progress_handlers.fire\n\t\t\t// fulfilled_handlers.fire\n\t\t\t// rejected_handlers.fire\n\t\t\tlist.add( tuple[ 3 ].fire );\n\n\t\t\t// deferred.notify = function() { deferred.notifyWith(...) }\n\t\t\t// deferred.resolve = function() { deferred.resolveWith(...) }\n\t\t\t// deferred.reject = function() { deferred.rejectWith(...) }\n\t\t\tdeferred[ tuple[ 0 ] ] = function() {\n\t\t\t\tdeferred[ tuple[ 0 ] + \"With\" ]( this === deferred ? undefined : this, arguments );\n\t\t\t\treturn this;\n\t\t\t};\n\n\t\t\t// deferred.notifyWith = list.fireWith\n\t\t\t// deferred.resolveWith = list.fireWith\n\t\t\t// deferred.rejectWith = list.fireWith\n\t\t\tdeferred[ tuple[ 0 ] + \"With\" ] = list.fireWith;\n\t\t} );\n\n\t\t// Make the deferred a promise\n\t\tpromise.promise( deferred );\n\n\t\t// Call given func if any\n\t\tif ( func ) {\n\t\t\tfunc.call( deferred, deferred );\n\t\t}\n\n\t\t// All done!\n\t\treturn deferred;\n\t},\n\n\t// Deferred helper\n\twhen: function( singleValue ) {\n\t\tvar\n\n\t\t\t// count of uncompleted subordinates\n\t\t\tremaining = arguments.length,\n\n\t\t\t// count of unprocessed arguments\n\t\t\ti = remaining,\n\n\t\t\t// subordinate fulfillment data\n\t\t\tresolveContexts = Array( i ),\n\t\t\tresolveValues = slice.call( arguments ),\n\n\t\t\t// the primary Deferred\n\t\t\tprimary = jQuery.Deferred(),\n\n\t\t\t// subordinate callback factory\n\t\t\tupdateFunc = function( i ) {\n\t\t\t\treturn function( value ) {\n\t\t\t\t\tresolveContexts[ i ] = this;\n\t\t\t\t\tresolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;\n\t\t\t\t\tif ( !( --remaining ) ) {\n\t\t\t\t\t\tprimary.resolveWith( resolveContexts, resolveValues );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t};\n\n\t\t// Single- and empty arguments are adopted like Promise.resolve\n\t\tif ( remaining <= 1 ) {\n\t\t\tadoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject,\n\t\t\t\t!remaining );\n\n\t\t\t// Use .then() to unwrap secondary thenables (cf. gh-3000)\n\t\t\tif ( primary.state() === \"pending\" ||\n\t\t\t\tisFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {\n\n\t\t\t\treturn primary.then();\n\t\t\t}\n\t\t}\n\n\t\t// Multiple arguments are aggregated like Promise.all array elements\n\t\twhile ( i-- ) {\n\t\t\tadoptValue( resolveValues[ i ], updateFunc( i ), primary.reject );\n\t\t}\n\n\t\treturn primary.promise();\n\t}\n} );\n\n\n// These usually indicate a programmer mistake during development,\n// warn about them ASAP rather than swallowing them by default.\nvar rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;\n\njQuery.Deferred.exceptionHook = function( error, stack ) {\n\n\t// Support: IE 8 - 9 only\n\t// Console exists when dev tools are open, which can happen at any time\n\tif ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) {\n\t\twindow.console.warn( \"jQuery.Deferred exception: \" + error.message, error.stack, stack );\n\t}\n};\n\n\n\n\njQuery.readyException = function( error ) {\n\twindow.setTimeout( function() {\n\t\tthrow error;\n\t} );\n};\n\n\n\n\n// The deferred used on DOM ready\nvar readyList = jQuery.Deferred();\n\njQuery.fn.ready = function( fn ) {\n\n\treadyList\n\t\t.then( fn )\n\n\t\t// Wrap jQuery.readyException in a function so that the lookup\n\t\t// happens at the time of error handling instead of callback\n\t\t// registration.\n\t\t.catch( function( error ) {\n\t\t\tjQuery.readyException( error );\n\t\t} );\n\n\treturn this;\n};\n\njQuery.extend( {\n\n\t// Is the DOM ready to be used? Set to true once it occurs.\n\tisReady: false,\n\n\t// A counter to track how many items to wait for before\n\t// the ready event fires. See trac-6781\n\treadyWait: 1,\n\n\t// Handle when the DOM is ready\n\tready: function( wait ) {\n\n\t\t// Abort if there are pending holds or we're already ready\n\t\tif ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Remember that the DOM is ready\n\t\tjQuery.isReady = true;\n\n\t\t// If a normal DOM Ready event fired, decrement, and wait if need be\n\t\tif ( wait !== true && --jQuery.readyWait > 0 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// If there are functions bound, to execute\n\t\treadyList.resolveWith( document, [ jQuery ] );\n\t}\n} );\n\njQuery.ready.then = readyList.then;\n\n// The ready event handler and self cleanup method\nfunction completed() {\n\tdocument.removeEventListener( \"DOMContentLoaded\", completed );\n\twindow.removeEventListener( \"load\", completed );\n\tjQuery.ready();\n}\n\n// Catch cases where $(document).ready() is called\n// after the browser event has already occurred.\n// Support: IE <=9 - 10 only\n// Older IE sometimes signals \"interactive\" too soon\nif ( document.readyState === \"complete\" ||\n\t( document.readyState !== \"loading\" && !document.documentElement.doScroll ) ) {\n\n\t// Handle it asynchronously to allow scripts the opportunity to delay ready\n\twindow.setTimeout( jQuery.ready );\n\n} else {\n\n\t// Use the handy event callback\n\tdocument.addEventListener( \"DOMContentLoaded\", completed );\n\n\t// A fallback to window.onload, that will always work\n\twindow.addEventListener( \"load\", completed );\n}\n\n\n\n\n// Multifunctional method to get and set values of a collection\n// The value/s can optionally be executed if it's a function\nvar access = function( elems, fn, key, value, chainable, emptyGet, raw ) {\n\tvar i = 0,\n\t\tlen = elems.length,\n\t\tbulk = key == null;\n\n\t// Sets many values\n\tif ( toType( key ) === \"object\" ) {\n\t\tchainable = true;\n\t\tfor ( i in key ) {\n\t\t\taccess( elems, fn, i, key[ i ], true, emptyGet, raw );\n\t\t}\n\n\t// Sets one value\n\t} else if ( value !== undefined ) {\n\t\tchainable = true;\n\n\t\tif ( !isFunction( value ) ) {\n\t\t\traw = true;\n\t\t}\n\n\t\tif ( bulk ) {\n\n\t\t\t// Bulk operations run against the entire set\n\t\t\tif ( raw ) {\n\t\t\t\tfn.call( elems, value );\n\t\t\t\tfn = null;\n\n\t\t\t// ...except when executing function values\n\t\t\t} else {\n\t\t\t\tbulk = fn;\n\t\t\t\tfn = function( elem, _key, value ) {\n\t\t\t\t\treturn bulk.call( jQuery( elem ), value );\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif ( fn ) {\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\tfn(\n\t\t\t\t\telems[ i ], key, raw ?\n\t\t\t\t\t\tvalue :\n\t\t\t\t\t\tvalue.call( elems[ i ], i, fn( elems[ i ], key ) )\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\tif ( chainable ) {\n\t\treturn elems;\n\t}\n\n\t// Gets\n\tif ( bulk ) {\n\t\treturn fn.call( elems );\n\t}\n\n\treturn len ? fn( elems[ 0 ], key ) : emptyGet;\n};\n\n\n// Matches dashed string for camelizing\nvar rmsPrefix = /^-ms-/,\n\trdashAlpha = /-([a-z])/g;\n\n// Used by camelCase as callback to replace()\nfunction fcamelCase( _all, letter ) {\n\treturn letter.toUpperCase();\n}\n\n// Convert dashed to camelCase; used by the css and data modules\n// Support: IE <=9 - 11, Edge 12 - 15\n// Microsoft forgot to hump their vendor prefix (trac-9572)\nfunction camelCase( string ) {\n\treturn string.replace( rmsPrefix, \"ms-\" ).replace( rdashAlpha, fcamelCase );\n}\nvar acceptData = function( owner ) {\n\n\t// Accepts only:\n\t//  - Node\n\t//    - Node.ELEMENT_NODE\n\t//    - Node.DOCUMENT_NODE\n\t//  - Object\n\t//    - Any\n\treturn owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );\n};\n\n\n\n\nfunction Data() {\n\tthis.expando = jQuery.expando + Data.uid++;\n}\n\nData.uid = 1;\n\nData.prototype = {\n\n\tcache: function( owner ) {\n\n\t\t// Check if the owner object already has a cache\n\t\tvar value = owner[ this.expando ];\n\n\t\t// If not, create one\n\t\tif ( !value ) {\n\t\t\tvalue = {};\n\n\t\t\t// We can accept data for non-element nodes in modern browsers,\n\t\t\t// but we should not, see trac-8335.\n\t\t\t// Always return an empty object.\n\t\t\tif ( acceptData( owner ) ) {\n\n\t\t\t\t// If it is a node unlikely to be stringify-ed or looped over\n\t\t\t\t// use plain assignment\n\t\t\t\tif ( owner.nodeType ) {\n\t\t\t\t\towner[ this.expando ] = value;\n\n\t\t\t\t// Otherwise secure it in a non-enumerable property\n\t\t\t\t// configurable must be true to allow the property to be\n\t\t\t\t// deleted when data is removed\n\t\t\t\t} else {\n\t\t\t\t\tObject.defineProperty( owner, this.expando, {\n\t\t\t\t\t\tvalue: value,\n\t\t\t\t\t\tconfigurable: true\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn value;\n\t},\n\tset: function( owner, data, value ) {\n\t\tvar prop,\n\t\t\tcache = this.cache( owner );\n\n\t\t// Handle: [ owner, key, value ] args\n\t\t// Always use camelCase key (gh-2257)\n\t\tif ( typeof data === \"string\" ) {\n\t\t\tcache[ camelCase( data ) ] = value;\n\n\t\t// Handle: [ owner, { properties } ] args\n\t\t} else {\n\n\t\t\t// Copy the properties one-by-one to the cache object\n\t\t\tfor ( prop in data ) {\n\t\t\t\tcache[ camelCase( prop ) ] = data[ prop ];\n\t\t\t}\n\t\t}\n\t\treturn cache;\n\t},\n\tget: function( owner, key ) {\n\t\treturn key === undefined ?\n\t\t\tthis.cache( owner ) :\n\n\t\t\t// Always use camelCase key (gh-2257)\n\t\t\towner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ];\n\t},\n\taccess: function( owner, key, value ) {\n\n\t\t// In cases where either:\n\t\t//\n\t\t//   1. No key was specified\n\t\t//   2. A string key was specified, but no value provided\n\t\t//\n\t\t// Take the \"read\" path and allow the get method to determine\n\t\t// which value to return, respectively either:\n\t\t//\n\t\t//   1. The entire cache object\n\t\t//   2. The data stored at the key\n\t\t//\n\t\tif ( key === undefined ||\n\t\t\t\t( ( key && typeof key === \"string\" ) && value === undefined ) ) {\n\n\t\t\treturn this.get( owner, key );\n\t\t}\n\n\t\t// When the key is not a string, or both a key and value\n\t\t// are specified, set or extend (existing objects) with either:\n\t\t//\n\t\t//   1. An object of properties\n\t\t//   2. A key and value\n\t\t//\n\t\tthis.set( owner, key, value );\n\n\t\t// Since the \"set\" path can have two possible entry points\n\t\t// return the expected data based on which path was taken[*]\n\t\treturn value !== undefined ? value : key;\n\t},\n\tremove: function( owner, key ) {\n\t\tvar i,\n\t\t\tcache = owner[ this.expando ];\n\n\t\tif ( cache === undefined ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( key !== undefined ) {\n\n\t\t\t// Support array or space separated string of keys\n\t\t\tif ( Array.isArray( key ) ) {\n\n\t\t\t\t// If key is an array of keys...\n\t\t\t\t// We always set camelCase keys, so remove that.\n\t\t\t\tkey = key.map( camelCase );\n\t\t\t} else {\n\t\t\t\tkey = camelCase( key );\n\n\t\t\t\t// If a key with the spaces exists, use it.\n\t\t\t\t// Otherwise, create an array by matching non-whitespace\n\t\t\t\tkey = key in cache ?\n\t\t\t\t\t[ key ] :\n\t\t\t\t\t( key.match( rnothtmlwhite ) || [] );\n\t\t\t}\n\n\t\t\ti = key.length;\n\n\t\t\twhile ( i-- ) {\n\t\t\t\tdelete cache[ key[ i ] ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove the expando if there's no more data\n\t\tif ( key === undefined || jQuery.isEmptyObject( cache ) ) {\n\n\t\t\t// Support: Chrome <=35 - 45\n\t\t\t// Webkit & Blink performance suffers when deleting properties\n\t\t\t// from DOM nodes, so set to undefined instead\n\t\t\t// https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted)\n\t\t\tif ( owner.nodeType ) {\n\t\t\t\towner[ this.expando ] = undefined;\n\t\t\t} else {\n\t\t\t\tdelete owner[ this.expando ];\n\t\t\t}\n\t\t}\n\t},\n\thasData: function( owner ) {\n\t\tvar cache = owner[ this.expando ];\n\t\treturn cache !== undefined && !jQuery.isEmptyObject( cache );\n\t}\n};\nvar dataPriv = new Data();\n\nvar dataUser = new Data();\n\n\n\n//\tImplementation Summary\n//\n//\t1. Enforce API surface and semantic compatibility with 1.9.x branch\n//\t2. Improve the module's maintainability by reducing the storage\n//\t\tpaths to a single mechanism.\n//\t3. Use the same single mechanism to support \"private\" and \"user\" data.\n//\t4. _Never_ expose \"private\" data to user code (TODO: Drop _data, _removeData)\n//\t5. Avoid exposing implementation details on user objects (eg. expando properties)\n//\t6. Provide a clear path for implementation upgrade to WeakMap in 2014\n\nvar rbrace = /^(?:\\{[\\w\\W]*\\}|\\[[\\w\\W]*\\])$/,\n\trmultiDash = /[A-Z]/g;\n\nfunction getData( data ) {\n\tif ( data === \"true\" ) {\n\t\treturn true;\n\t}\n\n\tif ( data === \"false\" ) {\n\t\treturn false;\n\t}\n\n\tif ( data === \"null\" ) {\n\t\treturn null;\n\t}\n\n\t// Only convert to a number if it doesn't change the string\n\tif ( data === +data + \"\" ) {\n\t\treturn +data;\n\t}\n\n\tif ( rbrace.test( data ) ) {\n\t\treturn JSON.parse( data );\n\t}\n\n\treturn data;\n}\n\nfunction dataAttr( elem, key, data ) {\n\tvar name;\n\n\t// If nothing was found internally, try to fetch any\n\t// data from the HTML5 data-* attribute\n\tif ( data === undefined && elem.nodeType === 1 ) {\n\t\tname = \"data-\" + key.replace( rmultiDash, \"-$&\" ).toLowerCase();\n\t\tdata = elem.getAttribute( name );\n\n\t\tif ( typeof data === \"string\" ) {\n\t\t\ttry {\n\t\t\t\tdata = getData( data );\n\t\t\t} catch ( e ) {}\n\n\t\t\t// Make sure we set the data so it isn't changed later\n\t\t\tdataUser.set( elem, key, data );\n\t\t} else {\n\t\t\tdata = undefined;\n\t\t}\n\t}\n\treturn data;\n}\n\njQuery.extend( {\n\thasData: function( elem ) {\n\t\treturn dataUser.hasData( elem ) || dataPriv.hasData( elem );\n\t},\n\n\tdata: function( elem, name, data ) {\n\t\treturn dataUser.access( elem, name, data );\n\t},\n\n\tremoveData: function( elem, name ) {\n\t\tdataUser.remove( elem, name );\n\t},\n\n\t// TODO: Now that all calls to _data and _removeData have been replaced\n\t// with direct calls to dataPriv methods, these can be deprecated.\n\t_data: function( elem, name, data ) {\n\t\treturn dataPriv.access( elem, name, data );\n\t},\n\n\t_removeData: function( elem, name ) {\n\t\tdataPriv.remove( elem, name );\n\t}\n} );\n\njQuery.fn.extend( {\n\tdata: function( key, value ) {\n\t\tvar i, name, data,\n\t\t\telem = this[ 0 ],\n\t\t\tattrs = elem && elem.attributes;\n\n\t\t// Gets all values\n\t\tif ( key === undefined ) {\n\t\t\tif ( this.length ) {\n\t\t\t\tdata = dataUser.get( elem );\n\n\t\t\t\tif ( elem.nodeType === 1 && !dataPriv.get( elem, \"hasDataAttrs\" ) ) {\n\t\t\t\t\ti = attrs.length;\n\t\t\t\t\twhile ( i-- ) {\n\n\t\t\t\t\t\t// Support: IE 11 only\n\t\t\t\t\t\t// The attrs elements can be null (trac-14894)\n\t\t\t\t\t\tif ( attrs[ i ] ) {\n\t\t\t\t\t\t\tname = attrs[ i ].name;\n\t\t\t\t\t\t\tif ( name.indexOf( \"data-\" ) === 0 ) {\n\t\t\t\t\t\t\t\tname = camelCase( name.slice( 5 ) );\n\t\t\t\t\t\t\t\tdataAttr( elem, name, data[ name ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdataPriv.set( elem, \"hasDataAttrs\", true );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn data;\n\t\t}\n\n\t\t// Sets multiple values\n\t\tif ( typeof key === \"object\" ) {\n\t\t\treturn this.each( function() {\n\t\t\t\tdataUser.set( this, key );\n\t\t\t} );\n\t\t}\n\n\t\treturn access( this, function( value ) {\n\t\t\tvar data;\n\n\t\t\t// The calling jQuery object (element matches) is not empty\n\t\t\t// (and therefore has an element appears at this[ 0 ]) and the\n\t\t\t// `value` parameter was not undefined. An empty jQuery object\n\t\t\t// will result in `undefined` for elem = this[ 0 ] which will\n\t\t\t// throw an exception if an attempt to read a data cache is made.\n\t\t\tif ( elem && value === undefined ) {\n\n\t\t\t\t// Attempt to get data from the cache\n\t\t\t\t// The key will always be camelCased in Data\n\t\t\t\tdata = dataUser.get( elem, key );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// Attempt to \"discover\" the data in\n\t\t\t\t// HTML5 custom data-* attrs\n\t\t\t\tdata = dataAttr( elem, key );\n\t\t\t\tif ( data !== undefined ) {\n\t\t\t\t\treturn data;\n\t\t\t\t}\n\n\t\t\t\t// We tried really hard, but the data doesn't exist.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Set the data...\n\t\t\tthis.each( function() {\n\n\t\t\t\t// We always store the camelCased key\n\t\t\t\tdataUser.set( this, key, value );\n\t\t\t} );\n\t\t}, null, value, arguments.length > 1, null, true );\n\t},\n\n\tremoveData: function( key ) {\n\t\treturn this.each( function() {\n\t\t\tdataUser.remove( this, key );\n\t\t} );\n\t}\n} );\n\n\njQuery.extend( {\n\tqueue: function( elem, type, data ) {\n\t\tvar queue;\n\n\t\tif ( elem ) {\n\t\t\ttype = ( type || \"fx\" ) + \"queue\";\n\t\t\tqueue = dataPriv.get( elem, type );\n\n\t\t\t// Speed up dequeue by getting out quickly if this is just a lookup\n\t\t\tif ( data ) {\n\t\t\t\tif ( !queue || Array.isArray( data ) ) {\n\t\t\t\t\tqueue = dataPriv.access( elem, type, jQuery.makeArray( data ) );\n\t\t\t\t} else {\n\t\t\t\t\tqueue.push( data );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn queue || [];\n\t\t}\n\t},\n\n\tdequeue: function( elem, type ) {\n\t\ttype = type || \"fx\";\n\n\t\tvar queue = jQuery.queue( elem, type ),\n\t\t\tstartLength = queue.length,\n\t\t\tfn = queue.shift(),\n\t\t\thooks = jQuery._queueHooks( elem, type ),\n\t\t\tnext = function() {\n\t\t\t\tjQuery.dequeue( elem, type );\n\t\t\t};\n\n\t\t// If the fx queue is dequeued, always remove the progress sentinel\n\t\tif ( fn === \"inprogress\" ) {\n\t\t\tfn = queue.shift();\n\t\t\tstartLength--;\n\t\t}\n\n\t\tif ( fn ) {\n\n\t\t\t// Add a progress sentinel to prevent the fx queue from being\n\t\t\t// automatically dequeued\n\t\t\tif ( type === \"fx\" ) {\n\t\t\t\tqueue.unshift( \"inprogress\" );\n\t\t\t}\n\n\t\t\t// Clear up the last queue stop function\n\t\t\tdelete hooks.stop;\n\t\t\tfn.call( elem, next, hooks );\n\t\t}\n\n\t\tif ( !startLength && hooks ) {\n\t\t\thooks.empty.fire();\n\t\t}\n\t},\n\n\t// Not public - generate a queueHooks object, or return the current one\n\t_queueHooks: function( elem, type ) {\n\t\tvar key = type + \"queueHooks\";\n\t\treturn dataPriv.get( elem, key ) || dataPriv.access( elem, key, {\n\t\t\tempty: jQuery.Callbacks( \"once memory\" ).add( function() {\n\t\t\t\tdataPriv.remove( elem, [ type + \"queue\", key ] );\n\t\t\t} )\n\t\t} );\n\t}\n} );\n\njQuery.fn.extend( {\n\tqueue: function( type, data ) {\n\t\tvar setter = 2;\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tdata = type;\n\t\t\ttype = \"fx\";\n\t\t\tsetter--;\n\t\t}\n\n\t\tif ( arguments.length < setter ) {\n\t\t\treturn jQuery.queue( this[ 0 ], type );\n\t\t}\n\n\t\treturn data === undefined ?\n\t\t\tthis :\n\t\t\tthis.each( function() {\n\t\t\t\tvar queue = jQuery.queue( this, type, data );\n\n\t\t\t\t// Ensure a hooks for this queue\n\t\t\t\tjQuery._queueHooks( this, type );\n\n\t\t\t\tif ( type === \"fx\" && queue[ 0 ] !== \"inprogress\" ) {\n\t\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t\t}\n\t\t\t} );\n\t},\n\tdequeue: function( type ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.dequeue( this, type );\n\t\t} );\n\t},\n\tclearQueue: function( type ) {\n\t\treturn this.queue( type || \"fx\", [] );\n\t},\n\n\t// Get a promise resolved when queues of a certain type\n\t// are emptied (fx is the type by default)\n\tpromise: function( type, obj ) {\n\t\tvar tmp,\n\t\t\tcount = 1,\n\t\t\tdefer = jQuery.Deferred(),\n\t\t\telements = this,\n\t\t\ti = this.length,\n\t\t\tresolve = function() {\n\t\t\t\tif ( !( --count ) ) {\n\t\t\t\t\tdefer.resolveWith( elements, [ elements ] );\n\t\t\t\t}\n\t\t\t};\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tobj = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\ttype = type || \"fx\";\n\n\t\twhile ( i-- ) {\n\t\t\ttmp = dataPriv.get( elements[ i ], type + \"queueHooks\" );\n\t\t\tif ( tmp && tmp.empty ) {\n\t\t\t\tcount++;\n\t\t\t\ttmp.empty.add( resolve );\n\t\t\t}\n\t\t}\n\t\tresolve();\n\t\treturn defer.promise( obj );\n\t}\n} );\nvar pnum = ( /[+-]?(?:\\d*\\.|)\\d+(?:[eE][+-]?\\d+|)/ ).source;\n\nvar rcssNum = new RegExp( \"^(?:([+-])=|)(\" + pnum + \")([a-z%]*)$\", \"i\" );\n\n\nvar cssExpand = [ \"Top\", \"Right\", \"Bottom\", \"Left\" ];\n\nvar documentElement = document.documentElement;\n\n\n\n\tvar isAttached = function( elem ) {\n\t\t\treturn jQuery.contains( elem.ownerDocument, elem );\n\t\t},\n\t\tcomposed = { composed: true };\n\n\t// Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only\n\t// Check attachment across shadow DOM boundaries when possible (gh-3504)\n\t// Support: iOS 10.0-10.2 only\n\t// Early iOS 10 versions support `attachShadow` but not `getRootNode`,\n\t// leading to errors. We need to check for `getRootNode`.\n\tif ( documentElement.getRootNode ) {\n\t\tisAttached = function( elem ) {\n\t\t\treturn jQuery.contains( elem.ownerDocument, elem ) ||\n\t\t\t\telem.getRootNode( composed ) === elem.ownerDocument;\n\t\t};\n\t}\nvar isHiddenWithinTree = function( elem, el ) {\n\n\t\t// isHiddenWithinTree might be called from jQuery#filter function;\n\t\t// in that case, element will be second argument\n\t\telem = el || elem;\n\n\t\t// Inline style trumps all\n\t\treturn elem.style.display === \"none\" ||\n\t\t\telem.style.display === \"\" &&\n\n\t\t\t// Otherwise, check computed style\n\t\t\t// Support: Firefox <=43 - 45\n\t\t\t// Disconnected elements can have computed display: none, so first confirm that elem is\n\t\t\t// in the document.\n\t\t\tisAttached( elem ) &&\n\n\t\t\tjQuery.css( elem, \"display\" ) === \"none\";\n\t};\n\n\n\nfunction adjustCSS( elem, prop, valueParts, tween ) {\n\tvar adjusted, scale,\n\t\tmaxIterations = 20,\n\t\tcurrentValue = tween ?\n\t\t\tfunction() {\n\t\t\t\treturn tween.cur();\n\t\t\t} :\n\t\t\tfunction() {\n\t\t\t\treturn jQuery.css( elem, prop, \"\" );\n\t\t\t},\n\t\tinitial = currentValue(),\n\t\tunit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" ),\n\n\t\t// Starting value computation is required for potential unit mismatches\n\t\tinitialInUnit = elem.nodeType &&\n\t\t\t( jQuery.cssNumber[ prop ] || unit !== \"px\" && +initial ) &&\n\t\t\trcssNum.exec( jQuery.css( elem, prop ) );\n\n\tif ( initialInUnit && initialInUnit[ 3 ] !== unit ) {\n\n\t\t// Support: Firefox <=54\n\t\t// Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144)\n\t\tinitial = initial / 2;\n\n\t\t// Trust units reported by jQuery.css\n\t\tunit = unit || initialInUnit[ 3 ];\n\n\t\t// Iteratively approximate from a nonzero starting point\n\t\tinitialInUnit = +initial || 1;\n\n\t\twhile ( maxIterations-- ) {\n\n\t\t\t// Evaluate and update our best guess (doubling guesses that zero out).\n\t\t\t// Finish if the scale equals or crosses 1 (making the old*new product non-positive).\n\t\t\tjQuery.style( elem, prop, initialInUnit + unit );\n\t\t\tif ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) {\n\t\t\t\tmaxIterations = 0;\n\t\t\t}\n\t\t\tinitialInUnit = initialInUnit / scale;\n\n\t\t}\n\n\t\tinitialInUnit = initialInUnit * 2;\n\t\tjQuery.style( elem, prop, initialInUnit + unit );\n\n\t\t// Make sure we update the tween properties later on\n\t\tvalueParts = valueParts || [];\n\t}\n\n\tif ( valueParts ) {\n\t\tinitialInUnit = +initialInUnit || +initial || 0;\n\n\t\t// Apply relative offset (+=/-=) if specified\n\t\tadjusted = valueParts[ 1 ] ?\n\t\t\tinitialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :\n\t\t\t+valueParts[ 2 ];\n\t\tif ( tween ) {\n\t\t\ttween.unit = unit;\n\t\t\ttween.start = initialInUnit;\n\t\t\ttween.end = adjusted;\n\t\t}\n\t}\n\treturn adjusted;\n}\n\n\nvar defaultDisplayMap = {};\n\nfunction getDefaultDisplay( elem ) {\n\tvar temp,\n\t\tdoc = elem.ownerDocument,\n\t\tnodeName = elem.nodeName,\n\t\tdisplay = defaultDisplayMap[ nodeName ];\n\n\tif ( display ) {\n\t\treturn display;\n\t}\n\n\ttemp = doc.body.appendChild( doc.createElement( nodeName ) );\n\tdisplay = jQuery.css( temp, \"display\" );\n\n\ttemp.parentNode.removeChild( temp );\n\n\tif ( display === \"none\" ) {\n\t\tdisplay = \"block\";\n\t}\n\tdefaultDisplayMap[ nodeName ] = display;\n\n\treturn display;\n}\n\nfunction showHide( elements, show ) {\n\tvar display, elem,\n\t\tvalues = [],\n\t\tindex = 0,\n\t\tlength = elements.length;\n\n\t// Determine new display value for elements that need to change\n\tfor ( ; index < length; index++ ) {\n\t\telem = elements[ index ];\n\t\tif ( !elem.style ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tdisplay = elem.style.display;\n\t\tif ( show ) {\n\n\t\t\t// Since we force visibility upon cascade-hidden elements, an immediate (and slow)\n\t\t\t// check is required in this first loop unless we have a nonempty display value (either\n\t\t\t// inline or about-to-be-restored)\n\t\t\tif ( display === \"none\" ) {\n\t\t\t\tvalues[ index ] = dataPriv.get( elem, \"display\" ) || null;\n\t\t\t\tif ( !values[ index ] ) {\n\t\t\t\t\telem.style.display = \"\";\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ( elem.style.display === \"\" && isHiddenWithinTree( elem ) ) {\n\t\t\t\tvalues[ index ] = getDefaultDisplay( elem );\n\t\t\t}\n\t\t} else {\n\t\t\tif ( display !== \"none\" ) {\n\t\t\t\tvalues[ index ] = \"none\";\n\n\t\t\t\t// Remember what we're overwriting\n\t\t\t\tdataPriv.set( elem, \"display\", display );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Set the display of the elements in a second loop to avoid constant reflow\n\tfor ( index = 0; index < length; index++ ) {\n\t\tif ( values[ index ] != null ) {\n\t\t\telements[ index ].style.display = values[ index ];\n\t\t}\n\t}\n\n\treturn elements;\n}\n\njQuery.fn.extend( {\n\tshow: function() {\n\t\treturn showHide( this, true );\n\t},\n\thide: function() {\n\t\treturn showHide( this );\n\t},\n\ttoggle: function( state ) {\n\t\tif ( typeof state === \"boolean\" ) {\n\t\t\treturn state ? this.show() : this.hide();\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tif ( isHiddenWithinTree( this ) ) {\n\t\t\t\tjQuery( this ).show();\n\t\t\t} else {\n\t\t\t\tjQuery( this ).hide();\n\t\t\t}\n\t\t} );\n\t}\n} );\nvar rcheckableType = ( /^(?:checkbox|radio)$/i );\n\nvar rtagName = ( /<([a-z][^\\/\\0>\\x20\\t\\r\\n\\f]*)/i );\n\nvar rscriptType = ( /^$|^module$|\\/(?:java|ecma)script/i );\n\n\n\n( function() {\n\tvar fragment = document.createDocumentFragment(),\n\t\tdiv = fragment.appendChild( document.createElement( \"div\" ) ),\n\t\tinput = document.createElement( \"input\" );\n\n\t// Support: Android 4.0 - 4.3 only\n\t// Check state lost if the name is set (trac-11217)\n\t// Support: Windows Web Apps (WWA)\n\t// `name` and `type` must use .setAttribute for WWA (trac-14901)\n\tinput.setAttribute( \"type\", \"radio\" );\n\tinput.setAttribute( \"checked\", \"checked\" );\n\tinput.setAttribute( \"name\", \"t\" );\n\n\tdiv.appendChild( input );\n\n\t// Support: Android <=4.1 only\n\t// Older WebKit doesn't clone checked state correctly in fragments\n\tsupport.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;\n\n\t// Support: IE <=11 only\n\t// Make sure textarea (and checkbox) defaultValue is properly cloned\n\tdiv.innerHTML = \"<textarea>x</textarea>\";\n\tsupport.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;\n\n\t// Support: IE <=9 only\n\t// IE <=9 replaces <option> tags with their contents when inserted outside of\n\t// the select element.\n\tdiv.innerHTML = \"<option></option>\";\n\tsupport.option = !!div.lastChild;\n} )();\n\n\n// We have to close these tags to support XHTML (trac-13200)\nvar wrapMap = {\n\n\t// XHTML parsers do not magically insert elements in the\n\t// same way that tag soup parsers do. So we cannot shorten\n\t// this by omitting <tbody> or other required elements.\n\tthead: [ 1, \"<table>\", \"</table>\" ],\n\tcol: [ 2, \"<table><colgroup>\", \"</colgroup></table>\" ],\n\ttr: [ 2, \"<table><tbody>\", \"</tbody></table>\" ],\n\ttd: [ 3, \"<table><tbody><tr>\", \"</tr></tbody></table>\" ],\n\n\t_default: [ 0, \"\", \"\" ]\n};\n\nwrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;\nwrapMap.th = wrapMap.td;\n\n// Support: IE <=9 only\nif ( !support.option ) {\n\twrapMap.optgroup = wrapMap.option = [ 1, \"<select multiple='multiple'>\", \"</select>\" ];\n}\n\n\nfunction getAll( context, tag ) {\n\n\t// Support: IE <=9 - 11 only\n\t// Use typeof to avoid zero-argument method invocation on host objects (trac-15151)\n\tvar ret;\n\n\tif ( typeof context.getElementsByTagName !== \"undefined\" ) {\n\t\tret = context.getElementsByTagName( tag || \"*\" );\n\n\t} else if ( typeof context.querySelectorAll !== \"undefined\" ) {\n\t\tret = context.querySelectorAll( tag || \"*\" );\n\n\t} else {\n\t\tret = [];\n\t}\n\n\tif ( tag === undefined || tag && nodeName( context, tag ) ) {\n\t\treturn jQuery.merge( [ context ], ret );\n\t}\n\n\treturn ret;\n}\n\n\n// Mark scripts as having already been evaluated\nfunction setGlobalEval( elems, refElements ) {\n\tvar i = 0,\n\t\tl = elems.length;\n\n\tfor ( ; i < l; i++ ) {\n\t\tdataPriv.set(\n\t\t\telems[ i ],\n\t\t\t\"globalEval\",\n\t\t\t!refElements || dataPriv.get( refElements[ i ], \"globalEval\" )\n\t\t);\n\t}\n}\n\n\nvar rhtml = /<|&#?\\w+;/;\n\nfunction buildFragment( elems, context, scripts, selection, ignored ) {\n\tvar elem, tmp, tag, wrap, attached, j,\n\t\tfragment = context.createDocumentFragment(),\n\t\tnodes = [],\n\t\ti = 0,\n\t\tl = elems.length;\n\n\tfor ( ; i < l; i++ ) {\n\t\telem = elems[ i ];\n\n\t\tif ( elem || elem === 0 ) {\n\n\t\t\t// Add nodes directly\n\t\t\tif ( toType( elem ) === \"object\" ) {\n\n\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\tjQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );\n\n\t\t\t// Convert non-html into a text node\n\t\t\t} else if ( !rhtml.test( elem ) ) {\n\t\t\t\tnodes.push( context.createTextNode( elem ) );\n\n\t\t\t// Convert html into DOM nodes\n\t\t\t} else {\n\t\t\t\ttmp = tmp || fragment.appendChild( context.createElement( \"div\" ) );\n\n\t\t\t\t// Deserialize a standard representation\n\t\t\t\ttag = ( rtagName.exec( elem ) || [ \"\", \"\" ] )[ 1 ].toLowerCase();\n\t\t\t\twrap = wrapMap[ tag ] || wrapMap._default;\n\t\t\t\ttmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];\n\n\t\t\t\t// Descend through wrappers to the right content\n\t\t\t\tj = wrap[ 0 ];\n\t\t\t\twhile ( j-- ) {\n\t\t\t\t\ttmp = tmp.lastChild;\n\t\t\t\t}\n\n\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\tjQuery.merge( nodes, tmp.childNodes );\n\n\t\t\t\t// Remember the top-level container\n\t\t\t\ttmp = fragment.firstChild;\n\n\t\t\t\t// Ensure the created nodes are orphaned (trac-12392)\n\t\t\t\ttmp.textContent = \"\";\n\t\t\t}\n\t\t}\n\t}\n\n\t// Remove wrapper from fragment\n\tfragment.textContent = \"\";\n\n\ti = 0;\n\twhile ( ( elem = nodes[ i++ ] ) ) {\n\n\t\t// Skip elements already in the context collection (trac-4087)\n\t\tif ( selection && jQuery.inArray( elem, selection ) > -1 ) {\n\t\t\tif ( ignored ) {\n\t\t\t\tignored.push( elem );\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tattached = isAttached( elem );\n\n\t\t// Append to fragment\n\t\ttmp = getAll( fragment.appendChild( elem ), \"script\" );\n\n\t\t// Preserve script evaluation history\n\t\tif ( attached ) {\n\t\t\tsetGlobalEval( tmp );\n\t\t}\n\n\t\t// Capture executables\n\t\tif ( scripts ) {\n\t\t\tj = 0;\n\t\t\twhile ( ( elem = tmp[ j++ ] ) ) {\n\t\t\t\tif ( rscriptType.test( elem.type || \"\" ) ) {\n\t\t\t\t\tscripts.push( elem );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fragment;\n}\n\n\nvar rtypenamespace = /^([^.]*)(?:\\.(.+)|)/;\n\nfunction returnTrue() {\n\treturn true;\n}\n\nfunction returnFalse() {\n\treturn false;\n}\n\n// Support: IE <=9 - 11+\n// focus() and blur() are asynchronous, except when they are no-op.\n// So expect focus to be synchronous when the element is already active,\n// and blur to be synchronous when the element is not already active.\n// (focus and blur are always synchronous in other supported browsers,\n// this just defines when we can count on it).\nfunction expectSync( elem, type ) {\n\treturn ( elem === safeActiveElement() ) === ( type === \"focus\" );\n}\n\n// Support: IE <=9 only\n// Accessing document.activeElement can throw unexpectedly\n// https://bugs.jquery.com/ticket/13393\nfunction safeActiveElement() {\n\ttry {\n\t\treturn document.activeElement;\n\t} catch ( err ) { }\n}\n\nfunction on( elem, types, selector, data, fn, one ) {\n\tvar origFn, type;\n\n\t// Types can be a map of types/handlers\n\tif ( typeof types === \"object\" ) {\n\n\t\t// ( types-Object, selector, data )\n\t\tif ( typeof selector !== \"string\" ) {\n\n\t\t\t// ( types-Object, data )\n\t\t\tdata = data || selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tfor ( type in types ) {\n\t\t\ton( elem, type, selector, data, types[ type ], one );\n\t\t}\n\t\treturn elem;\n\t}\n\n\tif ( data == null && fn == null ) {\n\n\t\t// ( types, fn )\n\t\tfn = selector;\n\t\tdata = selector = undefined;\n\t} else if ( fn == null ) {\n\t\tif ( typeof selector === \"string\" ) {\n\n\t\t\t// ( types, selector, fn )\n\t\t\tfn = data;\n\t\t\tdata = undefined;\n\t\t} else {\n\n\t\t\t// ( types, data, fn )\n\t\t\tfn = data;\n\t\t\tdata = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t}\n\tif ( fn === false ) {\n\t\tfn = returnFalse;\n\t} else if ( !fn ) {\n\t\treturn elem;\n\t}\n\n\tif ( one === 1 ) {\n\t\torigFn = fn;\n\t\tfn = function( event ) {\n\n\t\t\t// Can use an empty set, since event contains the info\n\t\t\tjQuery().off( event );\n\t\t\treturn origFn.apply( this, arguments );\n\t\t};\n\n\t\t// Use same guid so caller can remove using origFn\n\t\tfn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );\n\t}\n\treturn elem.each( function() {\n\t\tjQuery.event.add( this, types, fn, data, selector );\n\t} );\n}\n\n/*\n * Helper functions for managing events -- not part of the public interface.\n * Props to Dean Edwards' addEvent library for many of the ideas.\n */\njQuery.event = {\n\n\tglobal: {},\n\n\tadd: function( elem, types, handler, data, selector ) {\n\n\t\tvar handleObjIn, eventHandle, tmp,\n\t\t\tevents, t, handleObj,\n\t\t\tspecial, handlers, type, namespaces, origType,\n\t\t\telemData = dataPriv.get( elem );\n\n\t\t// Only attach events to objects that accept data\n\t\tif ( !acceptData( elem ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Caller can pass in an object of custom data in lieu of the handler\n\t\tif ( handler.handler ) {\n\t\t\thandleObjIn = handler;\n\t\t\thandler = handleObjIn.handler;\n\t\t\tselector = handleObjIn.selector;\n\t\t}\n\n\t\t// Ensure that invalid selectors throw exceptions at attach time\n\t\t// Evaluate against documentElement in case elem is a non-element node (e.g., document)\n\t\tif ( selector ) {\n\t\t\tjQuery.find.matchesSelector( documentElement, selector );\n\t\t}\n\n\t\t// Make sure that the handler has a unique ID, used to find/remove it later\n\t\tif ( !handler.guid ) {\n\t\t\thandler.guid = jQuery.guid++;\n\t\t}\n\n\t\t// Init the element's event structure and main handler, if this is the first\n\t\tif ( !( events = elemData.events ) ) {\n\t\t\tevents = elemData.events = Object.create( null );\n\t\t}\n\t\tif ( !( eventHandle = elemData.handle ) ) {\n\t\t\teventHandle = elemData.handle = function( e ) {\n\n\t\t\t\t// Discard the second event of a jQuery.event.trigger() and\n\t\t\t\t// when an event is called after a page has unloaded\n\t\t\t\treturn typeof jQuery !== \"undefined\" && jQuery.event.triggered !== e.type ?\n\t\t\t\t\tjQuery.event.dispatch.apply( elem, arguments ) : undefined;\n\t\t\t};\n\t\t}\n\n\t\t// Handle multiple events separated by a space\n\t\ttypes = ( types || \"\" ).match( rnothtmlwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[ t ] ) || [];\n\t\t\ttype = origType = tmp[ 1 ];\n\t\t\tnamespaces = ( tmp[ 2 ] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// There *must* be a type, no attaching namespace-only handlers\n\t\t\tif ( !type ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// If event changes its type, use the special event handlers for the changed type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// If selector defined, determine special event api type, otherwise given type\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\n\t\t\t// Update special based on newly reset type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// handleObj is passed to all event handlers\n\t\t\thandleObj = jQuery.extend( {\n\t\t\t\ttype: type,\n\t\t\t\torigType: origType,\n\t\t\t\tdata: data,\n\t\t\t\thandler: handler,\n\t\t\t\tguid: handler.guid,\n\t\t\t\tselector: selector,\n\t\t\t\tneedsContext: selector && jQuery.expr.match.needsContext.test( selector ),\n\t\t\t\tnamespace: namespaces.join( \".\" )\n\t\t\t}, handleObjIn );\n\n\t\t\t// Init the event handler queue if we're the first\n\t\t\tif ( !( handlers = events[ type ] ) ) {\n\t\t\t\thandlers = events[ type ] = [];\n\t\t\t\thandlers.delegateCount = 0;\n\n\t\t\t\t// Only use addEventListener if the special events handler returns false\n\t\t\t\tif ( !special.setup ||\n\t\t\t\t\tspecial.setup.call( elem, data, namespaces, eventHandle ) === false ) {\n\n\t\t\t\t\tif ( elem.addEventListener ) {\n\t\t\t\t\t\telem.addEventListener( type, eventHandle );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( special.add ) {\n\t\t\t\tspecial.add.call( elem, handleObj );\n\n\t\t\t\tif ( !handleObj.handler.guid ) {\n\t\t\t\t\thandleObj.handler.guid = handler.guid;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add to the element's handler list, delegates in front\n\t\t\tif ( selector ) {\n\t\t\t\thandlers.splice( handlers.delegateCount++, 0, handleObj );\n\t\t\t} else {\n\t\t\t\thandlers.push( handleObj );\n\t\t\t}\n\n\t\t\t// Keep track of which events have ever been used, for event optimization\n\t\t\tjQuery.event.global[ type ] = true;\n\t\t}\n\n\t},\n\n\t// Detach an event or set of events from an element\n\tremove: function( elem, types, handler, selector, mappedTypes ) {\n\n\t\tvar j, origCount, tmp,\n\t\t\tevents, t, handleObj,\n\t\t\tspecial, handlers, type, namespaces, origType,\n\t\t\telemData = dataPriv.hasData( elem ) && dataPriv.get( elem );\n\n\t\tif ( !elemData || !( events = elemData.events ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Once for each type.namespace in types; type may be omitted\n\t\ttypes = ( types || \"\" ).match( rnothtmlwhite ) || [ \"\" ];\n\t\tt = types.length;\n\t\twhile ( t-- ) {\n\t\t\ttmp = rtypenamespace.exec( types[ t ] ) || [];\n\t\t\ttype = origType = tmp[ 1 ];\n\t\t\tnamespaces = ( tmp[ 2 ] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// Unbind all events (on this namespace, if provided) for the element\n\t\t\tif ( !type ) {\n\t\t\t\tfor ( type in events ) {\n\t\t\t\t\tjQuery.event.remove( elem, type + types[ t ], handler, selector, true );\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\t\t\thandlers = events[ type ] || [];\n\t\t\ttmp = tmp[ 2 ] &&\n\t\t\t\tnew RegExp( \"(^|\\\\.)\" + namespaces.join( \"\\\\.(?:.*\\\\.|)\" ) + \"(\\\\.|$)\" );\n\n\t\t\t// Remove matching events\n\t\t\torigCount = j = handlers.length;\n\t\t\twhile ( j-- ) {\n\t\t\t\thandleObj = handlers[ j ];\n\n\t\t\t\tif ( ( mappedTypes || origType === handleObj.origType ) &&\n\t\t\t\t\t( !handler || handler.guid === handleObj.guid ) &&\n\t\t\t\t\t( !tmp || tmp.test( handleObj.namespace ) ) &&\n\t\t\t\t\t( !selector || selector === handleObj.selector ||\n\t\t\t\t\t\tselector === \"**\" && handleObj.selector ) ) {\n\t\t\t\t\thandlers.splice( j, 1 );\n\n\t\t\t\t\tif ( handleObj.selector ) {\n\t\t\t\t\t\thandlers.delegateCount--;\n\t\t\t\t\t}\n\t\t\t\t\tif ( special.remove ) {\n\t\t\t\t\t\tspecial.remove.call( elem, handleObj );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Remove generic event handler if we removed something and no more handlers exist\n\t\t\t// (avoids potential for endless recursion during removal of special event handlers)\n\t\t\tif ( origCount && !handlers.length ) {\n\t\t\t\tif ( !special.teardown ||\n\t\t\t\t\tspecial.teardown.call( elem, namespaces, elemData.handle ) === false ) {\n\n\t\t\t\t\tjQuery.removeEvent( elem, type, elemData.handle );\n\t\t\t\t}\n\n\t\t\t\tdelete events[ type ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove data and the expando if it's no longer used\n\t\tif ( jQuery.isEmptyObject( events ) ) {\n\t\t\tdataPriv.remove( elem, \"handle events\" );\n\t\t}\n\t},\n\n\tdispatch: function( nativeEvent ) {\n\n\t\tvar i, j, ret, matched, handleObj, handlerQueue,\n\t\t\targs = new Array( arguments.length ),\n\n\t\t\t// Make a writable jQuery.Event from the native event object\n\t\t\tevent = jQuery.event.fix( nativeEvent ),\n\n\t\t\thandlers = (\n\t\t\t\tdataPriv.get( this, \"events\" ) || Object.create( null )\n\t\t\t)[ event.type ] || [],\n\t\t\tspecial = jQuery.event.special[ event.type ] || {};\n\n\t\t// Use the fix-ed jQuery.Event rather than the (read-only) native event\n\t\targs[ 0 ] = event;\n\n\t\tfor ( i = 1; i < arguments.length; i++ ) {\n\t\t\targs[ i ] = arguments[ i ];\n\t\t}\n\n\t\tevent.delegateTarget = this;\n\n\t\t// Call the preDispatch hook for the mapped type, and let it bail if desired\n\t\tif ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine handlers\n\t\thandlerQueue = jQuery.event.handlers.call( this, event, handlers );\n\n\t\t// Run delegates first; they may want to stop propagation beneath us\n\t\ti = 0;\n\t\twhile ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {\n\t\t\tevent.currentTarget = matched.elem;\n\n\t\t\tj = 0;\n\t\t\twhile ( ( handleObj = matched.handlers[ j++ ] ) &&\n\t\t\t\t!event.isImmediatePropagationStopped() ) {\n\n\t\t\t\t// If the event is namespaced, then each handler is only invoked if it is\n\t\t\t\t// specially universal or its namespaces are a superset of the event's.\n\t\t\t\tif ( !event.rnamespace || handleObj.namespace === false ||\n\t\t\t\t\tevent.rnamespace.test( handleObj.namespace ) ) {\n\n\t\t\t\t\tevent.handleObj = handleObj;\n\t\t\t\t\tevent.data = handleObj.data;\n\n\t\t\t\t\tret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||\n\t\t\t\t\t\thandleObj.handler ).apply( matched.elem, args );\n\n\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\tif ( ( event.result = ret ) === false ) {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Call the postDispatch hook for the mapped type\n\t\tif ( special.postDispatch ) {\n\t\t\tspecial.postDispatch.call( this, event );\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\thandlers: function( event, handlers ) {\n\t\tvar i, handleObj, sel, matchedHandlers, matchedSelectors,\n\t\t\thandlerQueue = [],\n\t\t\tdelegateCount = handlers.delegateCount,\n\t\t\tcur = event.target;\n\n\t\t// Find delegate handlers\n\t\tif ( delegateCount &&\n\n\t\t\t// Support: IE <=9\n\t\t\t// Black-hole SVG <use> instance trees (trac-13180)\n\t\t\tcur.nodeType &&\n\n\t\t\t// Support: Firefox <=42\n\t\t\t// Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861)\n\t\t\t// https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click\n\t\t\t// Support: IE 11 only\n\t\t\t// ...but not arrow key \"clicks\" of radio inputs, which can have `button` -1 (gh-2343)\n\t\t\t!( event.type === \"click\" && event.button >= 1 ) ) {\n\n\t\t\tfor ( ; cur !== this; cur = cur.parentNode || this ) {\n\n\t\t\t\t// Don't check non-elements (trac-13208)\n\t\t\t\t// Don't process clicks on disabled elements (trac-6911, trac-8165, trac-11382, trac-11764)\n\t\t\t\tif ( cur.nodeType === 1 && !( event.type === \"click\" && cur.disabled === true ) ) {\n\t\t\t\t\tmatchedHandlers = [];\n\t\t\t\t\tmatchedSelectors = {};\n\t\t\t\t\tfor ( i = 0; i < delegateCount; i++ ) {\n\t\t\t\t\t\thandleObj = handlers[ i ];\n\n\t\t\t\t\t\t// Don't conflict with Object.prototype properties (trac-13203)\n\t\t\t\t\t\tsel = handleObj.selector + \" \";\n\n\t\t\t\t\t\tif ( matchedSelectors[ sel ] === undefined ) {\n\t\t\t\t\t\t\tmatchedSelectors[ sel ] = handleObj.needsContext ?\n\t\t\t\t\t\t\t\tjQuery( sel, this ).index( cur ) > -1 :\n\t\t\t\t\t\t\t\tjQuery.find( sel, this, null, [ cur ] ).length;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( matchedSelectors[ sel ] ) {\n\t\t\t\t\t\t\tmatchedHandlers.push( handleObj );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif ( matchedHandlers.length ) {\n\t\t\t\t\t\thandlerQueue.push( { elem: cur, handlers: matchedHandlers } );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add the remaining (directly-bound) handlers\n\t\tcur = this;\n\t\tif ( delegateCount < handlers.length ) {\n\t\t\thandlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } );\n\t\t}\n\n\t\treturn handlerQueue;\n\t},\n\n\taddProp: function( name, hook ) {\n\t\tObject.defineProperty( jQuery.Event.prototype, name, {\n\t\t\tenumerable: true,\n\t\t\tconfigurable: true,\n\n\t\t\tget: isFunction( hook ) ?\n\t\t\t\tfunction() {\n\t\t\t\t\tif ( this.originalEvent ) {\n\t\t\t\t\t\treturn hook( this.originalEvent );\n\t\t\t\t\t}\n\t\t\t\t} :\n\t\t\t\tfunction() {\n\t\t\t\t\tif ( this.originalEvent ) {\n\t\t\t\t\t\treturn this.originalEvent[ name ];\n\t\t\t\t\t}\n\t\t\t\t},\n\n\t\t\tset: function( value ) {\n\t\t\t\tObject.defineProperty( this, name, {\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t\twritable: true,\n\t\t\t\t\tvalue: value\n\t\t\t\t} );\n\t\t\t}\n\t\t} );\n\t},\n\n\tfix: function( originalEvent ) {\n\t\treturn originalEvent[ jQuery.expando ] ?\n\t\t\toriginalEvent :\n\t\t\tnew jQuery.Event( originalEvent );\n\t},\n\n\tspecial: {\n\t\tload: {\n\n\t\t\t// Prevent triggered image.load events from bubbling to window.load\n\t\t\tnoBubble: true\n\t\t},\n\t\tclick: {\n\n\t\t\t// Utilize native event to ensure correct state for checkable inputs\n\t\t\tsetup: function( data ) {\n\n\t\t\t\t// For mutual compressibility with _default, replace `this` access with a local var.\n\t\t\t\t// `|| data` is dead code meant only to preserve the variable through minification.\n\t\t\t\tvar el = this || data;\n\n\t\t\t\t// Claim the first handler\n\t\t\t\tif ( rcheckableType.test( el.type ) &&\n\t\t\t\t\tel.click && nodeName( el, \"input\" ) ) {\n\n\t\t\t\t\t// dataPriv.set( el, \"click\", ... )\n\t\t\t\t\tleverageNative( el, \"click\", returnTrue );\n\t\t\t\t}\n\n\t\t\t\t// Return false to allow normal processing in the caller\n\t\t\t\treturn false;\n\t\t\t},\n\t\t\ttrigger: function( data ) {\n\n\t\t\t\t// For mutual compressibility with _default, replace `this` access with a local var.\n\t\t\t\t// `|| data` is dead code meant only to preserve the variable through minification.\n\t\t\t\tvar el = this || data;\n\n\t\t\t\t// Force setup before triggering a click\n\t\t\t\tif ( rcheckableType.test( el.type ) &&\n\t\t\t\t\tel.click && nodeName( el, \"input\" ) ) {\n\n\t\t\t\t\tleverageNative( el, \"click\" );\n\t\t\t\t}\n\n\t\t\t\t// Return non-false to allow normal event-path propagation\n\t\t\t\treturn true;\n\t\t\t},\n\n\t\t\t// For cross-browser consistency, suppress native .click() on links\n\t\t\t// Also prevent it if we're currently inside a leveraged native-event stack\n\t\t\t_default: function( event ) {\n\t\t\t\tvar target = event.target;\n\t\t\t\treturn rcheckableType.test( target.type ) &&\n\t\t\t\t\ttarget.click && nodeName( target, \"input\" ) &&\n\t\t\t\t\tdataPriv.get( target, \"click\" ) ||\n\t\t\t\t\tnodeName( target, \"a\" );\n\t\t\t}\n\t\t},\n\n\t\tbeforeunload: {\n\t\t\tpostDispatch: function( event ) {\n\n\t\t\t\t// Support: Firefox 20+\n\t\t\t\t// Firefox doesn't alert if the returnValue field is not set.\n\t\t\t\tif ( event.result !== undefined && event.originalEvent ) {\n\t\t\t\t\tevent.originalEvent.returnValue = event.result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n};\n\n// Ensure the presence of an event listener that handles manually-triggered\n// synthetic events by interrupting progress until reinvoked in response to\n// *native* events that it fires directly, ensuring that state changes have\n// already occurred before other listeners are invoked.\nfunction leverageNative( el, type, expectSync ) {\n\n\t// Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add\n\tif ( !expectSync ) {\n\t\tif ( dataPriv.get( el, type ) === undefined ) {\n\t\t\tjQuery.event.add( el, type, returnTrue );\n\t\t}\n\t\treturn;\n\t}\n\n\t// Register the controller as a special universal handler for all event namespaces\n\tdataPriv.set( el, type, false );\n\tjQuery.event.add( el, type, {\n\t\tnamespace: false,\n\t\thandler: function( event ) {\n\t\t\tvar notAsync, result,\n\t\t\t\tsaved = dataPriv.get( this, type );\n\n\t\t\tif ( ( event.isTrigger & 1 ) && this[ type ] ) {\n\n\t\t\t\t// Interrupt processing of the outer synthetic .trigger()ed event\n\t\t\t\t// Saved data should be false in such cases, but might be a leftover capture object\n\t\t\t\t// from an async native handler (gh-4350)\n\t\t\t\tif ( !saved.length ) {\n\n\t\t\t\t\t// Store arguments for use when handling the inner native event\n\t\t\t\t\t// There will always be at least one argument (an event object), so this array\n\t\t\t\t\t// will not be confused with a leftover capture object.\n\t\t\t\t\tsaved = slice.call( arguments );\n\t\t\t\t\tdataPriv.set( this, type, saved );\n\n\t\t\t\t\t// Trigger the native event and capture its result\n\t\t\t\t\t// Support: IE <=9 - 11+\n\t\t\t\t\t// focus() and blur() are asynchronous\n\t\t\t\t\tnotAsync = expectSync( this, type );\n\t\t\t\t\tthis[ type ]();\n\t\t\t\t\tresult = dataPriv.get( this, type );\n\t\t\t\t\tif ( saved !== result || notAsync ) {\n\t\t\t\t\t\tdataPriv.set( this, type, false );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult = {};\n\t\t\t\t\t}\n\t\t\t\t\tif ( saved !== result ) {\n\n\t\t\t\t\t\t// Cancel the outer synthetic event\n\t\t\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t\t\t\tevent.preventDefault();\n\n\t\t\t\t\t\t// Support: Chrome 86+\n\t\t\t\t\t\t// In Chrome, if an element having a focusout handler is blurred by\n\t\t\t\t\t\t// clicking outside of it, it invokes the handler synchronously. If\n\t\t\t\t\t\t// that handler calls `.remove()` on the element, the data is cleared,\n\t\t\t\t\t\t// leaving `result` undefined. We need to guard against this.\n\t\t\t\t\t\treturn result && result.value;\n\t\t\t\t\t}\n\n\t\t\t\t// If this is an inner synthetic event for an event with a bubbling surrogate\n\t\t\t\t// (focus or blur), assume that the surrogate already propagated from triggering the\n\t\t\t\t// native event and prevent that from happening again here.\n\t\t\t\t// This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the\n\t\t\t\t// bubbling surrogate propagates *after* the non-bubbling base), but that seems\n\t\t\t\t// less bad than duplication.\n\t\t\t\t} else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) {\n\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t}\n\n\t\t\t// If this is a native event triggered above, everything is now in order\n\t\t\t// Fire an inner synthetic event with the original arguments\n\t\t\t} else if ( saved.length ) {\n\n\t\t\t\t// ...and capture the result\n\t\t\t\tdataPriv.set( this, type, {\n\t\t\t\t\tvalue: jQuery.event.trigger(\n\n\t\t\t\t\t\t// Support: IE <=9 - 11+\n\t\t\t\t\t\t// Extend with the prototype to reset the above stopImmediatePropagation()\n\t\t\t\t\t\tjQuery.extend( saved[ 0 ], jQuery.Event.prototype ),\n\t\t\t\t\t\tsaved.slice( 1 ),\n\t\t\t\t\t\tthis\n\t\t\t\t\t)\n\t\t\t\t} );\n\n\t\t\t\t// Abort handling of the native event\n\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t}\n\t\t}\n\t} );\n}\n\njQuery.removeEvent = function( elem, type, handle ) {\n\n\t// This \"if\" is needed for plain objects\n\tif ( elem.removeEventListener ) {\n\t\telem.removeEventListener( type, handle );\n\t}\n};\n\njQuery.Event = function( src, props ) {\n\n\t// Allow instantiation without the 'new' keyword\n\tif ( !( this instanceof jQuery.Event ) ) {\n\t\treturn new jQuery.Event( src, props );\n\t}\n\n\t// Event object\n\tif ( src && src.type ) {\n\t\tthis.originalEvent = src;\n\t\tthis.type = src.type;\n\n\t\t// Events bubbling up the document may have been marked as prevented\n\t\t// by a handler lower down the tree; reflect the correct value.\n\t\tthis.isDefaultPrevented = src.defaultPrevented ||\n\t\t\t\tsrc.defaultPrevented === undefined &&\n\n\t\t\t\t// Support: Android <=2.3 only\n\t\t\t\tsrc.returnValue === false ?\n\t\t\treturnTrue :\n\t\t\treturnFalse;\n\n\t\t// Create target properties\n\t\t// Support: Safari <=6 - 7 only\n\t\t// Target should not be a text node (trac-504, trac-13143)\n\t\tthis.target = ( src.target && src.target.nodeType === 3 ) ?\n\t\t\tsrc.target.parentNode :\n\t\t\tsrc.target;\n\n\t\tthis.currentTarget = src.currentTarget;\n\t\tthis.relatedTarget = src.relatedTarget;\n\n\t// Event type\n\t} else {\n\t\tthis.type = src;\n\t}\n\n\t// Put explicitly provided properties onto the event object\n\tif ( props ) {\n\t\tjQuery.extend( this, props );\n\t}\n\n\t// Create a timestamp if incoming event doesn't have one\n\tthis.timeStamp = src && src.timeStamp || Date.now();\n\n\t// Mark it as fixed\n\tthis[ jQuery.expando ] = true;\n};\n\n// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding\n// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html\njQuery.Event.prototype = {\n\tconstructor: jQuery.Event,\n\tisDefaultPrevented: returnFalse,\n\tisPropagationStopped: returnFalse,\n\tisImmediatePropagationStopped: returnFalse,\n\tisSimulated: false,\n\n\tpreventDefault: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isDefaultPrevented = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.preventDefault();\n\t\t}\n\t},\n\tstopPropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isPropagationStopped = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.stopPropagation();\n\t\t}\n\t},\n\tstopImmediatePropagation: function() {\n\t\tvar e = this.originalEvent;\n\n\t\tthis.isImmediatePropagationStopped = returnTrue;\n\n\t\tif ( e && !this.isSimulated ) {\n\t\t\te.stopImmediatePropagation();\n\t\t}\n\n\t\tthis.stopPropagation();\n\t}\n};\n\n// Includes all common event props including KeyEvent and MouseEvent specific props\njQuery.each( {\n\taltKey: true,\n\tbubbles: true,\n\tcancelable: true,\n\tchangedTouches: true,\n\tctrlKey: true,\n\tdetail: true,\n\teventPhase: true,\n\tmetaKey: true,\n\tpageX: true,\n\tpageY: true,\n\tshiftKey: true,\n\tview: true,\n\t\"char\": true,\n\tcode: true,\n\tcharCode: true,\n\tkey: true,\n\tkeyCode: true,\n\tbutton: true,\n\tbuttons: true,\n\tclientX: true,\n\tclientY: true,\n\toffsetX: true,\n\toffsetY: true,\n\tpointerId: true,\n\tpointerType: true,\n\tscreenX: true,\n\tscreenY: true,\n\ttargetTouches: true,\n\ttoElement: true,\n\ttouches: true,\n\twhich: true\n}, jQuery.event.addProp );\n\njQuery.each( { focus: \"focusin\", blur: \"focusout\" }, function( type, delegateType ) {\n\tjQuery.event.special[ type ] = {\n\n\t\t// Utilize native event if possible so blur/focus sequence is correct\n\t\tsetup: function() {\n\n\t\t\t// Claim the first handler\n\t\t\t// dataPriv.set( this, \"focus\", ... )\n\t\t\t// dataPriv.set( this, \"blur\", ... )\n\t\t\tleverageNative( this, type, expectSync );\n\n\t\t\t// Return false to allow normal processing in the caller\n\t\t\treturn false;\n\t\t},\n\t\ttrigger: function() {\n\n\t\t\t// Force setup before trigger\n\t\t\tleverageNative( this, type );\n\n\t\t\t// Return non-false to allow normal event-path propagation\n\t\t\treturn true;\n\t\t},\n\n\t\t// Suppress native focus or blur if we're currently inside\n\t\t// a leveraged native-event stack\n\t\t_default: function( event ) {\n\t\t\treturn dataPriv.get( event.target, type );\n\t\t},\n\n\t\tdelegateType: delegateType\n\t};\n} );\n\n// Create mouseenter/leave events using mouseover/out and event-time checks\n// so that event delegation works in jQuery.\n// Do the same for pointerenter/pointerleave and pointerover/pointerout\n//\n// Support: Safari 7 only\n// Safari sends mouseenter too often; see:\n// https://bugs.chromium.org/p/chromium/issues/detail?id=470258\n// for the description of the bug (it existed in older Chrome versions as well).\njQuery.each( {\n\tmouseenter: \"mouseover\",\n\tmouseleave: \"mouseout\",\n\tpointerenter: \"pointerover\",\n\tpointerleave: \"pointerout\"\n}, function( orig, fix ) {\n\tjQuery.event.special[ orig ] = {\n\t\tdelegateType: fix,\n\t\tbindType: fix,\n\n\t\thandle: function( event ) {\n\t\t\tvar ret,\n\t\t\t\ttarget = this,\n\t\t\t\trelated = event.relatedTarget,\n\t\t\t\thandleObj = event.handleObj;\n\n\t\t\t// For mouseenter/leave call the handler if related is outside the target.\n\t\t\t// NB: No relatedTarget if the mouse left/entered the browser window\n\t\t\tif ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) {\n\t\t\t\tevent.type = handleObj.origType;\n\t\t\t\tret = handleObj.handler.apply( this, arguments );\n\t\t\t\tevent.type = fix;\n\t\t\t}\n\t\t\treturn ret;\n\t\t}\n\t};\n} );\n\njQuery.fn.extend( {\n\n\ton: function( types, selector, data, fn ) {\n\t\treturn on( this, types, selector, data, fn );\n\t},\n\tone: function( types, selector, data, fn ) {\n\t\treturn on( this, types, selector, data, fn, 1 );\n\t},\n\toff: function( types, selector, fn ) {\n\t\tvar handleObj, type;\n\t\tif ( types && types.preventDefault && types.handleObj ) {\n\n\t\t\t// ( event )  dispatched jQuery.Event\n\t\t\thandleObj = types.handleObj;\n\t\t\tjQuery( types.delegateTarget ).off(\n\t\t\t\thandleObj.namespace ?\n\t\t\t\t\thandleObj.origType + \".\" + handleObj.namespace :\n\t\t\t\t\thandleObj.origType,\n\t\t\t\thandleObj.selector,\n\t\t\t\thandleObj.handler\n\t\t\t);\n\t\t\treturn this;\n\t\t}\n\t\tif ( typeof types === \"object\" ) {\n\n\t\t\t// ( types-object [, selector] )\n\t\t\tfor ( type in types ) {\n\t\t\t\tthis.off( type, selector, types[ type ] );\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\t\tif ( selector === false || typeof selector === \"function\" ) {\n\n\t\t\t// ( types [, fn] )\n\t\t\tfn = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tif ( fn === false ) {\n\t\t\tfn = returnFalse;\n\t\t}\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.remove( this, types, fn, selector );\n\t\t} );\n\t}\n} );\n\n\nvar\n\n\t// Support: IE <=10 - 11, Edge 12 - 13 only\n\t// In IE/Edge using regex groups here causes severe slowdowns.\n\t// See https://connect.microsoft.com/IE/feedback/details/1736512/\n\trnoInnerhtml = /<script|<style|<link/i,\n\n\t// checked=\"checked\" or checked\n\trchecked = /checked\\s*(?:[^=]|=\\s*.checked.)/i,\n\n\trcleanScript = /^\\s*<!\\[CDATA\\[|\\]\\]>\\s*$/g;\n\n// Prefer a tbody over its parent table for containing new rows\nfunction manipulationTarget( elem, content ) {\n\tif ( nodeName( elem, \"table\" ) &&\n\t\tnodeName( content.nodeType !== 11 ? content : content.firstChild, \"tr\" ) ) {\n\n\t\treturn jQuery( elem ).children( \"tbody\" )[ 0 ] || elem;\n\t}\n\n\treturn elem;\n}\n\n// Replace/restore the type attribute of script elements for safe DOM manipulation\nfunction disableScript( elem ) {\n\telem.type = ( elem.getAttribute( \"type\" ) !== null ) + \"/\" + elem.type;\n\treturn elem;\n}\nfunction restoreScript( elem ) {\n\tif ( ( elem.type || \"\" ).slice( 0, 5 ) === \"true/\" ) {\n\t\telem.type = elem.type.slice( 5 );\n\t} else {\n\t\telem.removeAttribute( \"type\" );\n\t}\n\n\treturn elem;\n}\n\nfunction cloneCopyEvent( src, dest ) {\n\tvar i, l, type, pdataOld, udataOld, udataCur, events;\n\n\tif ( dest.nodeType !== 1 ) {\n\t\treturn;\n\t}\n\n\t// 1. Copy private data: events, handlers, etc.\n\tif ( dataPriv.hasData( src ) ) {\n\t\tpdataOld = dataPriv.get( src );\n\t\tevents = pdataOld.events;\n\n\t\tif ( events ) {\n\t\t\tdataPriv.remove( dest, \"handle events\" );\n\n\t\t\tfor ( type in events ) {\n\t\t\t\tfor ( i = 0, l = events[ type ].length; i < l; i++ ) {\n\t\t\t\t\tjQuery.event.add( dest, type, events[ type ][ i ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// 2. Copy user data\n\tif ( dataUser.hasData( src ) ) {\n\t\tudataOld = dataUser.access( src );\n\t\tudataCur = jQuery.extend( {}, udataOld );\n\n\t\tdataUser.set( dest, udataCur );\n\t}\n}\n\n// Fix IE bugs, see support tests\nfunction fixInput( src, dest ) {\n\tvar nodeName = dest.nodeName.toLowerCase();\n\n\t// Fails to persist the checked state of a cloned checkbox or radio button.\n\tif ( nodeName === \"input\" && rcheckableType.test( src.type ) ) {\n\t\tdest.checked = src.checked;\n\n\t// Fails to return the selected option to the default selected state when cloning options\n\t} else if ( nodeName === \"input\" || nodeName === \"textarea\" ) {\n\t\tdest.defaultValue = src.defaultValue;\n\t}\n}\n\nfunction domManip( collection, args, callback, ignored ) {\n\n\t// Flatten any nested arrays\n\targs = flat( args );\n\n\tvar fragment, first, scripts, hasScripts, node, doc,\n\t\ti = 0,\n\t\tl = collection.length,\n\t\tiNoClone = l - 1,\n\t\tvalue = args[ 0 ],\n\t\tvalueIsFunction = isFunction( value );\n\n\t// We can't cloneNode fragments that contain checked, in WebKit\n\tif ( valueIsFunction ||\n\t\t\t( l > 1 && typeof value === \"string\" &&\n\t\t\t\t!support.checkClone && rchecked.test( value ) ) ) {\n\t\treturn collection.each( function( index ) {\n\t\t\tvar self = collection.eq( index );\n\t\t\tif ( valueIsFunction ) {\n\t\t\t\targs[ 0 ] = value.call( this, index, self.html() );\n\t\t\t}\n\t\t\tdomManip( self, args, callback, ignored );\n\t\t} );\n\t}\n\n\tif ( l ) {\n\t\tfragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored );\n\t\tfirst = fragment.firstChild;\n\n\t\tif ( fragment.childNodes.length === 1 ) {\n\t\t\tfragment = first;\n\t\t}\n\n\t\t// Require either new content or an interest in ignored elements to invoke the callback\n\t\tif ( first || ignored ) {\n\t\t\tscripts = jQuery.map( getAll( fragment, \"script\" ), disableScript );\n\t\t\thasScripts = scripts.length;\n\n\t\t\t// Use the original fragment for the last item\n\t\t\t// instead of the first because it can end up\n\t\t\t// being emptied incorrectly in certain situations (trac-8070).\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tnode = fragment;\n\n\t\t\t\tif ( i !== iNoClone ) {\n\t\t\t\t\tnode = jQuery.clone( node, true, true );\n\n\t\t\t\t\t// Keep references to cloned scripts for later restoration\n\t\t\t\t\tif ( hasScripts ) {\n\n\t\t\t\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t\t\t\t// push.apply(_, arraylike) throws on ancient WebKit\n\t\t\t\t\t\tjQuery.merge( scripts, getAll( node, \"script\" ) );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcallback.call( collection[ i ], node, i );\n\t\t\t}\n\n\t\t\tif ( hasScripts ) {\n\t\t\t\tdoc = scripts[ scripts.length - 1 ].ownerDocument;\n\n\t\t\t\t// Reenable scripts\n\t\t\t\tjQuery.map( scripts, restoreScript );\n\n\t\t\t\t// Evaluate executable scripts on first document insertion\n\t\t\t\tfor ( i = 0; i < hasScripts; i++ ) {\n\t\t\t\t\tnode = scripts[ i ];\n\t\t\t\t\tif ( rscriptType.test( node.type || \"\" ) &&\n\t\t\t\t\t\t!dataPriv.access( node, \"globalEval\" ) &&\n\t\t\t\t\t\tjQuery.contains( doc, node ) ) {\n\n\t\t\t\t\t\tif ( node.src && ( node.type || \"\" ).toLowerCase()  !== \"module\" ) {\n\n\t\t\t\t\t\t\t// Optional AJAX dependency, but won't run scripts if not present\n\t\t\t\t\t\t\tif ( jQuery._evalUrl && !node.noModule ) {\n\t\t\t\t\t\t\t\tjQuery._evalUrl( node.src, {\n\t\t\t\t\t\t\t\t\tnonce: node.nonce || node.getAttribute( \"nonce\" )\n\t\t\t\t\t\t\t\t}, doc );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// Unwrap a CDATA section containing script contents. This shouldn't be\n\t\t\t\t\t\t\t// needed as in XML documents they're already not visible when\n\t\t\t\t\t\t\t// inspecting element contents and in HTML documents they have no\n\t\t\t\t\t\t\t// meaning but we're preserving that logic for backwards compatibility.\n\t\t\t\t\t\t\t// This will be removed completely in 4.0. See gh-4904.\n\t\t\t\t\t\t\tDOMEval( node.textContent.replace( rcleanScript, \"\" ), node, doc );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn collection;\n}\n\nfunction remove( elem, selector, keepData ) {\n\tvar node,\n\t\tnodes = selector ? jQuery.filter( selector, elem ) : elem,\n\t\ti = 0;\n\n\tfor ( ; ( node = nodes[ i ] ) != null; i++ ) {\n\t\tif ( !keepData && node.nodeType === 1 ) {\n\t\t\tjQuery.cleanData( getAll( node ) );\n\t\t}\n\n\t\tif ( node.parentNode ) {\n\t\t\tif ( keepData && isAttached( node ) ) {\n\t\t\t\tsetGlobalEval( getAll( node, \"script\" ) );\n\t\t\t}\n\t\t\tnode.parentNode.removeChild( node );\n\t\t}\n\t}\n\n\treturn elem;\n}\n\njQuery.extend( {\n\thtmlPrefilter: function( html ) {\n\t\treturn html;\n\t},\n\n\tclone: function( elem, dataAndEvents, deepDataAndEvents ) {\n\t\tvar i, l, srcElements, destElements,\n\t\t\tclone = elem.cloneNode( true ),\n\t\t\tinPage = isAttached( elem );\n\n\t\t// Fix IE cloning issues\n\t\tif ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&\n\t\t\t\t!jQuery.isXMLDoc( elem ) ) {\n\n\t\t\t// We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2\n\t\t\tdestElements = getAll( clone );\n\t\t\tsrcElements = getAll( elem );\n\n\t\t\tfor ( i = 0, l = srcElements.length; i < l; i++ ) {\n\t\t\t\tfixInput( srcElements[ i ], destElements[ i ] );\n\t\t\t}\n\t\t}\n\n\t\t// Copy the events from the original to the clone\n\t\tif ( dataAndEvents ) {\n\t\t\tif ( deepDataAndEvents ) {\n\t\t\t\tsrcElements = srcElements || getAll( elem );\n\t\t\t\tdestElements = destElements || getAll( clone );\n\n\t\t\t\tfor ( i = 0, l = srcElements.length; i < l; i++ ) {\n\t\t\t\t\tcloneCopyEvent( srcElements[ i ], destElements[ i ] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcloneCopyEvent( elem, clone );\n\t\t\t}\n\t\t}\n\n\t\t// Preserve script evaluation history\n\t\tdestElements = getAll( clone, \"script\" );\n\t\tif ( destElements.length > 0 ) {\n\t\t\tsetGlobalEval( destElements, !inPage && getAll( elem, \"script\" ) );\n\t\t}\n\n\t\t// Return the cloned set\n\t\treturn clone;\n\t},\n\n\tcleanData: function( elems ) {\n\t\tvar data, elem, type,\n\t\t\tspecial = jQuery.event.special,\n\t\t\ti = 0;\n\n\t\tfor ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {\n\t\t\tif ( acceptData( elem ) ) {\n\t\t\t\tif ( ( data = elem[ dataPriv.expando ] ) ) {\n\t\t\t\t\tif ( data.events ) {\n\t\t\t\t\t\tfor ( type in data.events ) {\n\t\t\t\t\t\t\tif ( special[ type ] ) {\n\t\t\t\t\t\t\t\tjQuery.event.remove( elem, type );\n\n\t\t\t\t\t\t\t// This is a shortcut to avoid jQuery.event.remove's overhead\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tjQuery.removeEvent( elem, type, data.handle );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Support: Chrome <=35 - 45+\n\t\t\t\t\t// Assign undefined instead of using delete, see Data#remove\n\t\t\t\t\telem[ dataPriv.expando ] = undefined;\n\t\t\t\t}\n\t\t\t\tif ( elem[ dataUser.expando ] ) {\n\n\t\t\t\t\t// Support: Chrome <=35 - 45+\n\t\t\t\t\t// Assign undefined instead of using delete, see Data#remove\n\t\t\t\t\telem[ dataUser.expando ] = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n} );\n\njQuery.fn.extend( {\n\tdetach: function( selector ) {\n\t\treturn remove( this, selector, true );\n\t},\n\n\tremove: function( selector ) {\n\t\treturn remove( this, selector );\n\t},\n\n\ttext: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\treturn value === undefined ?\n\t\t\t\tjQuery.text( this ) :\n\t\t\t\tthis.empty().each( function() {\n\t\t\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\t\t\tthis.textContent = value;\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t}, null, value, arguments.length );\n\t},\n\n\tappend: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.appendChild( elem );\n\t\t\t}\n\t\t} );\n\t},\n\n\tprepend: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n\t\t\t\tvar target = manipulationTarget( this, elem );\n\t\t\t\ttarget.insertBefore( elem, target.firstChild );\n\t\t\t}\n\t\t} );\n\t},\n\n\tbefore: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this );\n\t\t\t}\n\t\t} );\n\t},\n\n\tafter: function() {\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tif ( this.parentNode ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this.nextSibling );\n\t\t\t}\n\t\t} );\n\t},\n\n\tempty: function() {\n\t\tvar elem,\n\t\t\ti = 0;\n\n\t\tfor ( ; ( elem = this[ i ] ) != null; i++ ) {\n\t\t\tif ( elem.nodeType === 1 ) {\n\n\t\t\t\t// Prevent memory leaks\n\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\n\t\t\t\t// Remove any remaining nodes\n\t\t\t\telem.textContent = \"\";\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tclone: function( dataAndEvents, deepDataAndEvents ) {\n\t\tdataAndEvents = dataAndEvents == null ? false : dataAndEvents;\n\t\tdeepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;\n\n\t\treturn this.map( function() {\n\t\t\treturn jQuery.clone( this, dataAndEvents, deepDataAndEvents );\n\t\t} );\n\t},\n\n\thtml: function( value ) {\n\t\treturn access( this, function( value ) {\n\t\t\tvar elem = this[ 0 ] || {},\n\t\t\t\ti = 0,\n\t\t\t\tl = this.length;\n\n\t\t\tif ( value === undefined && elem.nodeType === 1 ) {\n\t\t\t\treturn elem.innerHTML;\n\t\t\t}\n\n\t\t\t// See if we can take a shortcut and just use innerHTML\n\t\t\tif ( typeof value === \"string\" && !rnoInnerhtml.test( value ) &&\n\t\t\t\t!wrapMap[ ( rtagName.exec( value ) || [ \"\", \"\" ] )[ 1 ].toLowerCase() ] ) {\n\n\t\t\t\tvalue = jQuery.htmlPrefilter( value );\n\n\t\t\t\ttry {\n\t\t\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\t\t\telem = this[ i ] || {};\n\n\t\t\t\t\t\t// Remove element nodes and prevent memory leaks\n\t\t\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\t\t\tjQuery.cleanData( getAll( elem, false ) );\n\t\t\t\t\t\t\telem.innerHTML = value;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\telem = 0;\n\n\t\t\t\t// If using innerHTML throws an exception, use the fallback method\n\t\t\t\t} catch ( e ) {}\n\t\t\t}\n\n\t\t\tif ( elem ) {\n\t\t\t\tthis.empty().append( value );\n\t\t\t}\n\t\t}, null, value, arguments.length );\n\t},\n\n\treplaceWith: function() {\n\t\tvar ignored = [];\n\n\t\t// Make the changes, replacing each non-ignored context element with the new content\n\t\treturn domManip( this, arguments, function( elem ) {\n\t\t\tvar parent = this.parentNode;\n\n\t\t\tif ( jQuery.inArray( this, ignored ) < 0 ) {\n\t\t\t\tjQuery.cleanData( getAll( this ) );\n\t\t\t\tif ( parent ) {\n\t\t\t\t\tparent.replaceChild( elem, this );\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Force callback invocation\n\t\t}, ignored );\n\t}\n} );\n\njQuery.each( {\n\tappendTo: \"append\",\n\tprependTo: \"prepend\",\n\tinsertBefore: \"before\",\n\tinsertAfter: \"after\",\n\treplaceAll: \"replaceWith\"\n}, function( name, original ) {\n\tjQuery.fn[ name ] = function( selector ) {\n\t\tvar elems,\n\t\t\tret = [],\n\t\t\tinsert = jQuery( selector ),\n\t\t\tlast = insert.length - 1,\n\t\t\ti = 0;\n\n\t\tfor ( ; i <= last; i++ ) {\n\t\t\telems = i === last ? this : this.clone( true );\n\t\t\tjQuery( insert[ i ] )[ original ]( elems );\n\n\t\t\t// Support: Android <=4.0 only, PhantomJS 1 only\n\t\t\t// .get() because push.apply(_, arraylike) throws on ancient WebKit\n\t\t\tpush.apply( ret, elems.get() );\n\t\t}\n\n\t\treturn this.pushStack( ret );\n\t};\n} );\nvar rnumnonpx = new RegExp( \"^(\" + pnum + \")(?!px)[a-z%]+$\", \"i\" );\n\nvar rcustomProp = /^--/;\n\n\nvar getStyles = function( elem ) {\n\n\t\t// Support: IE <=11 only, Firefox <=30 (trac-15098, trac-14150)\n\t\t// IE throws on elements created in popups\n\t\t// FF meanwhile throws on frame elements through \"defaultView.getComputedStyle\"\n\t\tvar view = elem.ownerDocument.defaultView;\n\n\t\tif ( !view || !view.opener ) {\n\t\t\tview = window;\n\t\t}\n\n\t\treturn view.getComputedStyle( elem );\n\t};\n\nvar swap = function( elem, options, callback ) {\n\tvar ret, name,\n\t\told = {};\n\n\t// Remember the old values, and insert the new ones\n\tfor ( name in options ) {\n\t\told[ name ] = elem.style[ name ];\n\t\telem.style[ name ] = options[ name ];\n\t}\n\n\tret = callback.call( elem );\n\n\t// Revert the old values\n\tfor ( name in options ) {\n\t\telem.style[ name ] = old[ name ];\n\t}\n\n\treturn ret;\n};\n\n\nvar rboxStyle = new RegExp( cssExpand.join( \"|\" ), \"i\" );\n\nvar whitespace = \"[\\\\x20\\\\t\\\\r\\\\n\\\\f]\";\n\n\nvar rtrimCSS = new RegExp(\n\t\"^\" + whitespace + \"+|((?:^|[^\\\\\\\\])(?:\\\\\\\\.)*)\" + whitespace + \"+$\",\n\t\"g\"\n);\n\n\n\n\n( function() {\n\n\t// Executing both pixelPosition & boxSizingReliable tests require only one layout\n\t// so they're executed at the same time to save the second computation.\n\tfunction computeStyleTests() {\n\n\t\t// This is a singleton, we need to execute it only once\n\t\tif ( !div ) {\n\t\t\treturn;\n\t\t}\n\n\t\tcontainer.style.cssText = \"position:absolute;left:-11111px;width:60px;\" +\n\t\t\t\"margin-top:1px;padding:0;border:0\";\n\t\tdiv.style.cssText =\n\t\t\t\"position:relative;display:block;box-sizing:border-box;overflow:scroll;\" +\n\t\t\t\"margin:auto;border:1px;padding:1px;\" +\n\t\t\t\"width:60%;top:1%\";\n\t\tdocumentElement.appendChild( container ).appendChild( div );\n\n\t\tvar divStyle = window.getComputedStyle( div );\n\t\tpixelPositionVal = divStyle.top !== \"1%\";\n\n\t\t// Support: Android 4.0 - 4.3 only, Firefox <=3 - 44\n\t\treliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12;\n\n\t\t// Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3\n\t\t// Some styles come back with percentage values, even though they shouldn't\n\t\tdiv.style.right = \"60%\";\n\t\tpixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36;\n\n\t\t// Support: IE 9 - 11 only\n\t\t// Detect misreporting of content dimensions for box-sizing:border-box elements\n\t\tboxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36;\n\n\t\t// Support: IE 9 only\n\t\t// Detect overflow:scroll screwiness (gh-3699)\n\t\t// Support: Chrome <=64\n\t\t// Don't get tricked when zoom affects offsetWidth (gh-4029)\n\t\tdiv.style.position = \"absolute\";\n\t\tscrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12;\n\n\t\tdocumentElement.removeChild( container );\n\n\t\t// Nullify the div so it wouldn't be stored in the memory and\n\t\t// it will also be a sign that checks already performed\n\t\tdiv = null;\n\t}\n\n\tfunction roundPixelMeasures( measure ) {\n\t\treturn Math.round( parseFloat( measure ) );\n\t}\n\n\tvar pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal,\n\t\treliableTrDimensionsVal, reliableMarginLeftVal,\n\t\tcontainer = document.createElement( \"div\" ),\n\t\tdiv = document.createElement( \"div\" );\n\n\t// Finish early in limited (non-browser) environments\n\tif ( !div.style ) {\n\t\treturn;\n\t}\n\n\t// Support: IE <=9 - 11 only\n\t// Style of cloned element affects source element cloned (trac-8908)\n\tdiv.style.backgroundClip = \"content-box\";\n\tdiv.cloneNode( true ).style.backgroundClip = \"\";\n\tsupport.clearCloneStyle = div.style.backgroundClip === \"content-box\";\n\n\tjQuery.extend( support, {\n\t\tboxSizingReliable: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn boxSizingReliableVal;\n\t\t},\n\t\tpixelBoxStyles: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn pixelBoxStylesVal;\n\t\t},\n\t\tpixelPosition: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn pixelPositionVal;\n\t\t},\n\t\treliableMarginLeft: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn reliableMarginLeftVal;\n\t\t},\n\t\tscrollboxSize: function() {\n\t\t\tcomputeStyleTests();\n\t\t\treturn scrollboxSizeVal;\n\t\t},\n\n\t\t// Support: IE 9 - 11+, Edge 15 - 18+\n\t\t// IE/Edge misreport `getComputedStyle` of table rows with width/height\n\t\t// set in CSS while `offset*` properties report correct values.\n\t\t// Behavior in IE 9 is more subtle than in newer versions & it passes\n\t\t// some versions of this test; make sure not to make it pass there!\n\t\t//\n\t\t// Support: Firefox 70+\n\t\t// Only Firefox includes border widths\n\t\t// in computed dimensions. (gh-4529)\n\t\treliableTrDimensions: function() {\n\t\t\tvar table, tr, trChild, trStyle;\n\t\t\tif ( reliableTrDimensionsVal == null ) {\n\t\t\t\ttable = document.createElement( \"table\" );\n\t\t\t\ttr = document.createElement( \"tr\" );\n\t\t\t\ttrChild = document.createElement( \"div\" );\n\n\t\t\t\ttable.style.cssText = \"position:absolute;left:-11111px;border-collapse:separate\";\n\t\t\t\ttr.style.cssText = \"border:1px solid\";\n\n\t\t\t\t// Support: Chrome 86+\n\t\t\t\t// Height set through cssText does not get applied.\n\t\t\t\t// Computed height then comes back as 0.\n\t\t\t\ttr.style.height = \"1px\";\n\t\t\t\ttrChild.style.height = \"9px\";\n\n\t\t\t\t// Support: Android 8 Chrome 86+\n\t\t\t\t// In our bodyBackground.html iframe,\n\t\t\t\t// display for all div elements is set to \"inline\",\n\t\t\t\t// which causes a problem only in Android 8 Chrome 86.\n\t\t\t\t// Ensuring the div is display: block\n\t\t\t\t// gets around this issue.\n\t\t\t\ttrChild.style.display = \"block\";\n\n\t\t\t\tdocumentElement\n\t\t\t\t\t.appendChild( table )\n\t\t\t\t\t.appendChild( tr )\n\t\t\t\t\t.appendChild( trChild );\n\n\t\t\t\ttrStyle = window.getComputedStyle( tr );\n\t\t\t\treliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) +\n\t\t\t\t\tparseInt( trStyle.borderTopWidth, 10 ) +\n\t\t\t\t\tparseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight;\n\n\t\t\t\tdocumentElement.removeChild( table );\n\t\t\t}\n\t\t\treturn reliableTrDimensionsVal;\n\t\t}\n\t} );\n} )();\n\n\nfunction curCSS( elem, name, computed ) {\n\tvar width, minWidth, maxWidth, ret,\n\t\tisCustomProp = rcustomProp.test( name ),\n\n\t\t// Support: Firefox 51+\n\t\t// Retrieving style before computed somehow\n\t\t// fixes an issue with getting wrong values\n\t\t// on detached elements\n\t\tstyle = elem.style;\n\n\tcomputed = computed || getStyles( elem );\n\n\t// getPropertyValue is needed for:\n\t//   .css('filter') (IE 9 only, trac-12537)\n\t//   .css('--customProperty) (gh-3144)\n\tif ( computed ) {\n\n\t\t// Support: IE <=9 - 11+\n\t\t// IE only supports `\"float\"` in `getPropertyValue`; in computed styles\n\t\t// it's only available as `\"cssFloat\"`. We no longer modify properties\n\t\t// sent to `.css()` apart from camelCasing, so we need to check both.\n\t\t// Normally, this would create difference in behavior: if\n\t\t// `getPropertyValue` returns an empty string, the value returned\n\t\t// by `.css()` would be `undefined`. This is usually the case for\n\t\t// disconnected elements. However, in IE even disconnected elements\n\t\t// with no styles return `\"none\"` for `getPropertyValue( \"float\" )`\n\t\tret = computed.getPropertyValue( name ) || computed[ name ];\n\n\t\tif ( isCustomProp && ret ) {\n\n\t\t\t// Support: Firefox 105+, Chrome <=105+\n\t\t\t// Spec requires trimming whitespace for custom properties (gh-4926).\n\t\t\t// Firefox only trims leading whitespace. Chrome just collapses\n\t\t\t// both leading & trailing whitespace to a single space.\n\t\t\t//\n\t\t\t// Fall back to `undefined` if empty string returned.\n\t\t\t// This collapses a missing definition with property defined\n\t\t\t// and set to an empty string but there's no standard API\n\t\t\t// allowing us to differentiate them without a performance penalty\n\t\t\t// and returning `undefined` aligns with older jQuery.\n\t\t\t//\n\t\t\t// rtrimCSS treats U+000D CARRIAGE RETURN and U+000C FORM FEED\n\t\t\t// as whitespace while CSS does not, but this is not a problem\n\t\t\t// because CSS preprocessing replaces them with U+000A LINE FEED\n\t\t\t// (which *is* CSS whitespace)\n\t\t\t// https://www.w3.org/TR/css-syntax-3/#input-preprocessing\n\t\t\tret = ret.replace( rtrimCSS, \"$1\" ) || undefined;\n\t\t}\n\n\t\tif ( ret === \"\" && !isAttached( elem ) ) {\n\t\t\tret = jQuery.style( elem, name );\n\t\t}\n\n\t\t// A tribute to the \"awesome hack by Dean Edwards\"\n\t\t// Android Browser returns percentage for some values,\n\t\t// but width seems to be reliably pixels.\n\t\t// This is against the CSSOM draft spec:\n\t\t// https://drafts.csswg.org/cssom/#resolved-values\n\t\tif ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) {\n\n\t\t\t// Remember the original values\n\t\t\twidth = style.width;\n\t\t\tminWidth = style.minWidth;\n\t\t\tmaxWidth = style.maxWidth;\n\n\t\t\t// Put in the new values to get a computed value out\n\t\t\tstyle.minWidth = style.maxWidth = style.width = ret;\n\t\t\tret = computed.width;\n\n\t\t\t// Revert the changed values\n\t\t\tstyle.width = width;\n\t\t\tstyle.minWidth = minWidth;\n\t\t\tstyle.maxWidth = maxWidth;\n\t\t}\n\t}\n\n\treturn ret !== undefined ?\n\n\t\t// Support: IE <=9 - 11 only\n\t\t// IE returns zIndex value as an integer.\n\t\tret + \"\" :\n\t\tret;\n}\n\n\nfunction addGetHookIf( conditionFn, hookFn ) {\n\n\t// Define the hook, we'll check on the first run if it's really needed.\n\treturn {\n\t\tget: function() {\n\t\t\tif ( conditionFn() ) {\n\n\t\t\t\t// Hook not needed (or it's not possible to use it due\n\t\t\t\t// to missing dependency), remove it.\n\t\t\t\tdelete this.get;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Hook needed; redefine it so that the support test is not executed again.\n\t\t\treturn ( this.get = hookFn ).apply( this, arguments );\n\t\t}\n\t};\n}\n\n\nvar cssPrefixes = [ \"Webkit\", \"Moz\", \"ms\" ],\n\temptyStyle = document.createElement( \"div\" ).style,\n\tvendorProps = {};\n\n// Return a vendor-prefixed property or undefined\nfunction vendorPropName( name ) {\n\n\t// Check for vendor prefixed names\n\tvar capName = name[ 0 ].toUpperCase() + name.slice( 1 ),\n\t\ti = cssPrefixes.length;\n\n\twhile ( i-- ) {\n\t\tname = cssPrefixes[ i ] + capName;\n\t\tif ( name in emptyStyle ) {\n\t\t\treturn name;\n\t\t}\n\t}\n}\n\n// Return a potentially-mapped jQuery.cssProps or vendor prefixed property\nfunction finalPropName( name ) {\n\tvar final = jQuery.cssProps[ name ] || vendorProps[ name ];\n\n\tif ( final ) {\n\t\treturn final;\n\t}\n\tif ( name in emptyStyle ) {\n\t\treturn name;\n\t}\n\treturn vendorProps[ name ] = vendorPropName( name ) || name;\n}\n\n\nvar\n\n\t// Swappable if display is none or starts with table\n\t// except \"table\", \"table-cell\", or \"table-caption\"\n\t// See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display\n\trdisplayswap = /^(none|table(?!-c[ea]).+)/,\n\tcssShow = { position: \"absolute\", visibility: \"hidden\", display: \"block\" },\n\tcssNormalTransform = {\n\t\tletterSpacing: \"0\",\n\t\tfontWeight: \"400\"\n\t};\n\nfunction setPositiveNumber( _elem, value, subtract ) {\n\n\t// Any relative (+/-) values have already been\n\t// normalized at this point\n\tvar matches = rcssNum.exec( value );\n\treturn matches ?\n\n\t\t// Guard against undefined \"subtract\", e.g., when used as in cssHooks\n\t\tMath.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || \"px\" ) :\n\t\tvalue;\n}\n\nfunction boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) {\n\tvar i = dimension === \"width\" ? 1 : 0,\n\t\textra = 0,\n\t\tdelta = 0;\n\n\t// Adjustment may not be necessary\n\tif ( box === ( isBorderBox ? \"border\" : \"content\" ) ) {\n\t\treturn 0;\n\t}\n\n\tfor ( ; i < 4; i += 2 ) {\n\n\t\t// Both box models exclude margin\n\t\tif ( box === \"margin\" ) {\n\t\t\tdelta += jQuery.css( elem, box + cssExpand[ i ], true, styles );\n\t\t}\n\n\t\t// If we get here with a content-box, we're seeking \"padding\" or \"border\" or \"margin\"\n\t\tif ( !isBorderBox ) {\n\n\t\t\t// Add padding\n\t\t\tdelta += jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\n\t\t\t// For \"border\" or \"margin\", add border\n\t\t\tif ( box !== \"padding\" ) {\n\t\t\t\tdelta += jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\n\t\t\t// But still keep track of it otherwise\n\t\t\t} else {\n\t\t\t\textra += jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\n\t\t// If we get here with a border-box (content + padding + border), we're seeking \"content\" or\n\t\t// \"padding\" or \"margin\"\n\t\t} else {\n\n\t\t\t// For \"content\", subtract padding\n\t\t\tif ( box === \"content\" ) {\n\t\t\t\tdelta -= jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\t\t\t}\n\n\t\t\t// For \"content\" or \"padding\", subtract border\n\t\t\tif ( box !== \"margin\" ) {\n\t\t\t\tdelta -= jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Account for positive content-box scroll gutter when requested by providing computedVal\n\tif ( !isBorderBox && computedVal >= 0 ) {\n\n\t\t// offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border\n\t\t// Assuming integer scroll gutter, subtract the rest and round down\n\t\tdelta += Math.max( 0, Math.ceil(\n\t\t\telem[ \"offset\" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -\n\t\t\tcomputedVal -\n\t\t\tdelta -\n\t\t\textra -\n\t\t\t0.5\n\n\t\t// If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter\n\t\t// Use an explicit zero to avoid NaN (gh-3964)\n\t\t) ) || 0;\n\t}\n\n\treturn delta;\n}\n\nfunction getWidthOrHeight( elem, dimension, extra ) {\n\n\t// Start with computed style\n\tvar styles = getStyles( elem ),\n\n\t\t// To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322).\n\t\t// Fake content-box until we know it's needed to know the true value.\n\t\tboxSizingNeeded = !support.boxSizingReliable() || extra,\n\t\tisBorderBox = boxSizingNeeded &&\n\t\t\tjQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\",\n\t\tvalueIsBorderBox = isBorderBox,\n\n\t\tval = curCSS( elem, dimension, styles ),\n\t\toffsetProp = \"offset\" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 );\n\n\t// Support: Firefox <=54\n\t// Return a confounding non-pixel value or feign ignorance, as appropriate.\n\tif ( rnumnonpx.test( val ) ) {\n\t\tif ( !extra ) {\n\t\t\treturn val;\n\t\t}\n\t\tval = \"auto\";\n\t}\n\n\n\t// Support: IE 9 - 11 only\n\t// Use offsetWidth/offsetHeight for when box sizing is unreliable.\n\t// In those cases, the computed value can be trusted to be border-box.\n\tif ( ( !support.boxSizingReliable() && isBorderBox ||\n\n\t\t// Support: IE 10 - 11+, Edge 15 - 18+\n\t\t// IE/Edge misreport `getComputedStyle` of table rows with width/height\n\t\t// set in CSS while `offset*` properties report correct values.\n\t\t// Interestingly, in some cases IE 9 doesn't suffer from this issue.\n\t\t!support.reliableTrDimensions() && nodeName( elem, \"tr\" ) ||\n\n\t\t// Fall back to offsetWidth/offsetHeight when value is \"auto\"\n\t\t// This happens for inline elements with no explicit setting (gh-3571)\n\t\tval === \"auto\" ||\n\n\t\t// Support: Android <=4.1 - 4.3 only\n\t\t// Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602)\n\t\t!parseFloat( val ) && jQuery.css( elem, \"display\", false, styles ) === \"inline\" ) &&\n\n\t\t// Make sure the element is visible & connected\n\t\telem.getClientRects().length ) {\n\n\t\tisBorderBox = jQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\";\n\n\t\t// Where available, offsetWidth/offsetHeight approximate border box dimensions.\n\t\t// Where not available (e.g., SVG), assume unreliable box-sizing and interpret the\n\t\t// retrieved value as a content box dimension.\n\t\tvalueIsBorderBox = offsetProp in elem;\n\t\tif ( valueIsBorderBox ) {\n\t\t\tval = elem[ offsetProp ];\n\t\t}\n\t}\n\n\t// Normalize \"\" and auto\n\tval = parseFloat( val ) || 0;\n\n\t// Adjust for the element's box model\n\treturn ( val +\n\t\tboxModelAdjustment(\n\t\t\telem,\n\t\t\tdimension,\n\t\t\textra || ( isBorderBox ? \"border\" : \"content\" ),\n\t\t\tvalueIsBorderBox,\n\t\t\tstyles,\n\n\t\t\t// Provide the current computed size to request scroll gutter calculation (gh-3589)\n\t\t\tval\n\t\t)\n\t) + \"px\";\n}\n\njQuery.extend( {\n\n\t// Add in style property hooks for overriding the default\n\t// behavior of getting and setting a style property\n\tcssHooks: {\n\t\topacity: {\n\t\t\tget: function( elem, computed ) {\n\t\t\t\tif ( computed ) {\n\n\t\t\t\t\t// We should always get a number back from opacity\n\t\t\t\t\tvar ret = curCSS( elem, \"opacity\" );\n\t\t\t\t\treturn ret === \"\" ? \"1\" : ret;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\t// Don't automatically add \"px\" to these possibly-unitless properties\n\tcssNumber: {\n\t\t\"animationIterationCount\": true,\n\t\t\"columnCount\": true,\n\t\t\"fillOpacity\": true,\n\t\t\"flexGrow\": true,\n\t\t\"flexShrink\": true,\n\t\t\"fontWeight\": true,\n\t\t\"gridArea\": true,\n\t\t\"gridColumn\": true,\n\t\t\"gridColumnEnd\": true,\n\t\t\"gridColumnStart\": true,\n\t\t\"gridRow\": true,\n\t\t\"gridRowEnd\": true,\n\t\t\"gridRowStart\": true,\n\t\t\"lineHeight\": true,\n\t\t\"opacity\": true,\n\t\t\"order\": true,\n\t\t\"orphans\": true,\n\t\t\"widows\": true,\n\t\t\"zIndex\": true,\n\t\t\"zoom\": true\n\t},\n\n\t// Add in properties whose names you wish to fix before\n\t// setting or getting the value\n\tcssProps: {},\n\n\t// Get and set the style property on a DOM Node\n\tstyle: function( elem, name, value, extra ) {\n\n\t\t// Don't set styles on text and comment nodes\n\t\tif ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Make sure that we're working with the right name\n\t\tvar ret, type, hooks,\n\t\t\torigName = camelCase( name ),\n\t\t\tisCustomProp = rcustomProp.test( name ),\n\t\t\tstyle = elem.style;\n\n\t\t// Make sure that we're working with the right name. We don't\n\t\t// want to query the value if it is a CSS custom property\n\t\t// since they are user-defined.\n\t\tif ( !isCustomProp ) {\n\t\t\tname = finalPropName( origName );\n\t\t}\n\n\t\t// Gets hook for the prefixed version, then unprefixed version\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// Check if we're setting a value\n\t\tif ( value !== undefined ) {\n\t\t\ttype = typeof value;\n\n\t\t\t// Convert \"+=\" or \"-=\" to relative numbers (trac-7345)\n\t\t\tif ( type === \"string\" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {\n\t\t\t\tvalue = adjustCSS( elem, name, ret );\n\n\t\t\t\t// Fixes bug trac-9237\n\t\t\t\ttype = \"number\";\n\t\t\t}\n\n\t\t\t// Make sure that null and NaN values aren't set (trac-7116)\n\t\t\tif ( value == null || value !== value ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// If a number was passed in, add the unit (except for certain CSS properties)\n\t\t\t// The isCustomProp check can be removed in jQuery 4.0 when we only auto-append\n\t\t\t// \"px\" to a few hardcoded values.\n\t\t\tif ( type === \"number\" && !isCustomProp ) {\n\t\t\t\tvalue += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? \"\" : \"px\" );\n\t\t\t}\n\n\t\t\t// background-* props affect original clone's values\n\t\t\tif ( !support.clearCloneStyle && value === \"\" && name.indexOf( \"background\" ) === 0 ) {\n\t\t\t\tstyle[ name ] = \"inherit\";\n\t\t\t}\n\n\t\t\t// If a hook was provided, use that value, otherwise just set the specified value\n\t\t\tif ( !hooks || !( \"set\" in hooks ) ||\n\t\t\t\t( value = hooks.set( elem, value, extra ) ) !== undefined ) {\n\n\t\t\t\tif ( isCustomProp ) {\n\t\t\t\t\tstyle.setProperty( name, value );\n\t\t\t\t} else {\n\t\t\t\t\tstyle[ name ] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t} else {\n\n\t\t\t// If a hook was provided get the non-computed value from there\n\t\t\tif ( hooks && \"get\" in hooks &&\n\t\t\t\t( ret = hooks.get( elem, false, extra ) ) !== undefined ) {\n\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\t// Otherwise just get the value from the style object\n\t\t\treturn style[ name ];\n\t\t}\n\t},\n\n\tcss: function( elem, name, extra, styles ) {\n\t\tvar val, num, hooks,\n\t\t\torigName = camelCase( name ),\n\t\t\tisCustomProp = rcustomProp.test( name );\n\n\t\t// Make sure that we're working with the right name. We don't\n\t\t// want to modify the value if it is a CSS custom property\n\t\t// since they are user-defined.\n\t\tif ( !isCustomProp ) {\n\t\t\tname = finalPropName( origName );\n\t\t}\n\n\t\t// Try prefixed name followed by the unprefixed name\n\t\thooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n\t\t// If a hook was provided get the computed value from there\n\t\tif ( hooks && \"get\" in hooks ) {\n\t\t\tval = hooks.get( elem, true, extra );\n\t\t}\n\n\t\t// Otherwise, if a way to get the computed value exists, use that\n\t\tif ( val === undefined ) {\n\t\t\tval = curCSS( elem, name, styles );\n\t\t}\n\n\t\t// Convert \"normal\" to computed value\n\t\tif ( val === \"normal\" && name in cssNormalTransform ) {\n\t\t\tval = cssNormalTransform[ name ];\n\t\t}\n\n\t\t// Make numeric if forced or a qualifier was provided and val looks numeric\n\t\tif ( extra === \"\" || extra ) {\n\t\t\tnum = parseFloat( val );\n\t\t\treturn extra === true || isFinite( num ) ? num || 0 : val;\n\t\t}\n\n\t\treturn val;\n\t}\n} );\n\njQuery.each( [ \"height\", \"width\" ], function( _i, dimension ) {\n\tjQuery.cssHooks[ dimension ] = {\n\t\tget: function( elem, computed, extra ) {\n\t\t\tif ( computed ) {\n\n\t\t\t\t// Certain elements can have dimension info if we invisibly show them\n\t\t\t\t// but it must have a current display style that would benefit\n\t\t\t\treturn rdisplayswap.test( jQuery.css( elem, \"display\" ) ) &&\n\n\t\t\t\t\t// Support: Safari 8+\n\t\t\t\t\t// Table columns in Safari have non-zero offsetWidth & zero\n\t\t\t\t\t// getBoundingClientRect().width unless display is changed.\n\t\t\t\t\t// Support: IE <=11 only\n\t\t\t\t\t// Running getBoundingClientRect on a disconnected node\n\t\t\t\t\t// in IE throws an error.\n\t\t\t\t\t( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ?\n\t\t\t\t\tswap( elem, cssShow, function() {\n\t\t\t\t\t\treturn getWidthOrHeight( elem, dimension, extra );\n\t\t\t\t\t} ) :\n\t\t\t\t\tgetWidthOrHeight( elem, dimension, extra );\n\t\t\t}\n\t\t},\n\n\t\tset: function( elem, value, extra ) {\n\t\t\tvar matches,\n\t\t\t\tstyles = getStyles( elem ),\n\n\t\t\t\t// Only read styles.position if the test has a chance to fail\n\t\t\t\t// to avoid forcing a reflow.\n\t\t\t\tscrollboxSizeBuggy = !support.scrollboxSize() &&\n\t\t\t\t\tstyles.position === \"absolute\",\n\n\t\t\t\t// To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991)\n\t\t\t\tboxSizingNeeded = scrollboxSizeBuggy || extra,\n\t\t\t\tisBorderBox = boxSizingNeeded &&\n\t\t\t\t\tjQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\",\n\t\t\t\tsubtract = extra ?\n\t\t\t\t\tboxModelAdjustment(\n\t\t\t\t\t\telem,\n\t\t\t\t\t\tdimension,\n\t\t\t\t\t\textra,\n\t\t\t\t\t\tisBorderBox,\n\t\t\t\t\t\tstyles\n\t\t\t\t\t) :\n\t\t\t\t\t0;\n\n\t\t\t// Account for unreliable border-box dimensions by comparing offset* to computed and\n\t\t\t// faking a content-box to get border and padding (gh-3699)\n\t\t\tif ( isBorderBox && scrollboxSizeBuggy ) {\n\t\t\t\tsubtract -= Math.ceil(\n\t\t\t\t\telem[ \"offset\" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -\n\t\t\t\t\tparseFloat( styles[ dimension ] ) -\n\t\t\t\t\tboxModelAdjustment( elem, dimension, \"border\", false, styles ) -\n\t\t\t\t\t0.5\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Convert to pixels if value adjustment is needed\n\t\t\tif ( subtract && ( matches = rcssNum.exec( value ) ) &&\n\t\t\t\t( matches[ 3 ] || \"px\" ) !== \"px\" ) {\n\n\t\t\t\telem.style[ dimension ] = value;\n\t\t\t\tvalue = jQuery.css( elem, dimension );\n\t\t\t}\n\n\t\t\treturn setPositiveNumber( elem, value, subtract );\n\t\t}\n\t};\n} );\n\njQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,\n\tfunction( elem, computed ) {\n\t\tif ( computed ) {\n\t\t\treturn ( parseFloat( curCSS( elem, \"marginLeft\" ) ) ||\n\t\t\t\telem.getBoundingClientRect().left -\n\t\t\t\t\tswap( elem, { marginLeft: 0 }, function() {\n\t\t\t\t\t\treturn elem.getBoundingClientRect().left;\n\t\t\t\t\t} )\n\t\t\t) + \"px\";\n\t\t}\n\t}\n);\n\n// These hooks are used by animate to expand properties\njQuery.each( {\n\tmargin: \"\",\n\tpadding: \"\",\n\tborder: \"Width\"\n}, function( prefix, suffix ) {\n\tjQuery.cssHooks[ prefix + suffix ] = {\n\t\texpand: function( value ) {\n\t\t\tvar i = 0,\n\t\t\t\texpanded = {},\n\n\t\t\t\t// Assumes a single number if not a string\n\t\t\t\tparts = typeof value === \"string\" ? value.split( \" \" ) : [ value ];\n\n\t\t\tfor ( ; i < 4; i++ ) {\n\t\t\t\texpanded[ prefix + cssExpand[ i ] + suffix ] =\n\t\t\t\t\tparts[ i ] || parts[ i - 2 ] || parts[ 0 ];\n\t\t\t}\n\n\t\t\treturn expanded;\n\t\t}\n\t};\n\n\tif ( prefix !== \"margin\" ) {\n\t\tjQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;\n\t}\n} );\n\njQuery.fn.extend( {\n\tcss: function( name, value ) {\n\t\treturn access( this, function( elem, name, value ) {\n\t\t\tvar styles, len,\n\t\t\t\tmap = {},\n\t\t\t\ti = 0;\n\n\t\t\tif ( Array.isArray( name ) ) {\n\t\t\t\tstyles = getStyles( elem );\n\t\t\t\tlen = name.length;\n\n\t\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\t\tmap[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );\n\t\t\t\t}\n\n\t\t\t\treturn map;\n\t\t\t}\n\n\t\t\treturn value !== undefined ?\n\t\t\t\tjQuery.style( elem, name, value ) :\n\t\t\t\tjQuery.css( elem, name );\n\t\t}, name, value, arguments.length > 1 );\n\t}\n} );\n\n\nfunction Tween( elem, options, prop, end, easing ) {\n\treturn new Tween.prototype.init( elem, options, prop, end, easing );\n}\njQuery.Tween = Tween;\n\nTween.prototype = {\n\tconstructor: Tween,\n\tinit: function( elem, options, prop, end, easing, unit ) {\n\t\tthis.elem = elem;\n\t\tthis.prop = prop;\n\t\tthis.easing = easing || jQuery.easing._default;\n\t\tthis.options = options;\n\t\tthis.start = this.now = this.cur();\n\t\tthis.end = end;\n\t\tthis.unit = unit || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" );\n\t},\n\tcur: function() {\n\t\tvar hooks = Tween.propHooks[ this.prop ];\n\n\t\treturn hooks && hooks.get ?\n\t\t\thooks.get( this ) :\n\t\t\tTween.propHooks._default.get( this );\n\t},\n\trun: function( percent ) {\n\t\tvar eased,\n\t\t\thooks = Tween.propHooks[ this.prop ];\n\n\t\tif ( this.options.duration ) {\n\t\t\tthis.pos = eased = jQuery.easing[ this.easing ](\n\t\t\t\tpercent, this.options.duration * percent, 0, 1, this.options.duration\n\t\t\t);\n\t\t} else {\n\t\t\tthis.pos = eased = percent;\n\t\t}\n\t\tthis.now = ( this.end - this.start ) * eased + this.start;\n\n\t\tif ( this.options.step ) {\n\t\t\tthis.options.step.call( this.elem, this.now, this );\n\t\t}\n\n\t\tif ( hooks && hooks.set ) {\n\t\t\thooks.set( this );\n\t\t} else {\n\t\t\tTween.propHooks._default.set( this );\n\t\t}\n\t\treturn this;\n\t}\n};\n\nTween.prototype.init.prototype = Tween.prototype;\n\nTween.propHooks = {\n\t_default: {\n\t\tget: function( tween ) {\n\t\t\tvar result;\n\n\t\t\t// Use a property on the element directly when it is not a DOM element,\n\t\t\t// or when there is no matching style property that exists.\n\t\t\tif ( tween.elem.nodeType !== 1 ||\n\t\t\t\ttween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) {\n\t\t\t\treturn tween.elem[ tween.prop ];\n\t\t\t}\n\n\t\t\t// Passing an empty string as a 3rd parameter to .css will automatically\n\t\t\t// attempt a parseFloat and fallback to a string if the parse fails.\n\t\t\t// Simple values such as \"10px\" are parsed to Float;\n\t\t\t// complex values such as \"rotate(1rad)\" are returned as-is.\n\t\t\tresult = jQuery.css( tween.elem, tween.prop, \"\" );\n\n\t\t\t// Empty strings, null, undefined and \"auto\" are converted to 0.\n\t\t\treturn !result || result === \"auto\" ? 0 : result;\n\t\t},\n\t\tset: function( tween ) {\n\n\t\t\t// Use step hook for back compat.\n\t\t\t// Use cssHook if its there.\n\t\t\t// Use .style if available and use plain properties where available.\n\t\t\tif ( jQuery.fx.step[ tween.prop ] ) {\n\t\t\t\tjQuery.fx.step[ tween.prop ]( tween );\n\t\t\t} else if ( tween.elem.nodeType === 1 && (\n\t\t\t\tjQuery.cssHooks[ tween.prop ] ||\n\t\t\t\t\ttween.elem.style[ finalPropName( tween.prop ) ] != null ) ) {\n\t\t\t\tjQuery.style( tween.elem, tween.prop, tween.now + tween.unit );\n\t\t\t} else {\n\t\t\t\ttween.elem[ tween.prop ] = tween.now;\n\t\t\t}\n\t\t}\n\t}\n};\n\n// Support: IE <=9 only\n// Panic based approach to setting things on disconnected nodes\nTween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {\n\tset: function( tween ) {\n\t\tif ( tween.elem.nodeType && tween.elem.parentNode ) {\n\t\t\ttween.elem[ tween.prop ] = tween.now;\n\t\t}\n\t}\n};\n\njQuery.easing = {\n\tlinear: function( p ) {\n\t\treturn p;\n\t},\n\tswing: function( p ) {\n\t\treturn 0.5 - Math.cos( p * Math.PI ) / 2;\n\t},\n\t_default: \"swing\"\n};\n\njQuery.fx = Tween.prototype.init;\n\n// Back compat <1.8 extension point\njQuery.fx.step = {};\n\n\n\n\nvar\n\tfxNow, inProgress,\n\trfxtypes = /^(?:toggle|show|hide)$/,\n\trrun = /queueHooks$/;\n\nfunction schedule() {\n\tif ( inProgress ) {\n\t\tif ( document.hidden === false && window.requestAnimationFrame ) {\n\t\t\twindow.requestAnimationFrame( schedule );\n\t\t} else {\n\t\t\twindow.setTimeout( schedule, jQuery.fx.interval );\n\t\t}\n\n\t\tjQuery.fx.tick();\n\t}\n}\n\n// Animations created synchronously will run synchronously\nfunction createFxNow() {\n\twindow.setTimeout( function() {\n\t\tfxNow = undefined;\n\t} );\n\treturn ( fxNow = Date.now() );\n}\n\n// Generate parameters to create a standard animation\nfunction genFx( type, includeWidth ) {\n\tvar which,\n\t\ti = 0,\n\t\tattrs = { height: type };\n\n\t// If we include width, step value is 1 to do all cssExpand values,\n\t// otherwise step value is 2 to skip over Left and Right\n\tincludeWidth = includeWidth ? 1 : 0;\n\tfor ( ; i < 4; i += 2 - includeWidth ) {\n\t\twhich = cssExpand[ i ];\n\t\tattrs[ \"margin\" + which ] = attrs[ \"padding\" + which ] = type;\n\t}\n\n\tif ( includeWidth ) {\n\t\tattrs.opacity = attrs.width = type;\n\t}\n\n\treturn attrs;\n}\n\nfunction createTween( value, prop, animation ) {\n\tvar tween,\n\t\tcollection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ \"*\" ] ),\n\t\tindex = 0,\n\t\tlength = collection.length;\n\tfor ( ; index < length; index++ ) {\n\t\tif ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {\n\n\t\t\t// We're done with this property\n\t\t\treturn tween;\n\t\t}\n\t}\n}\n\nfunction defaultPrefilter( elem, props, opts ) {\n\tvar prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display,\n\t\tisBox = \"width\" in props || \"height\" in props,\n\t\tanim = this,\n\t\torig = {},\n\t\tstyle = elem.style,\n\t\thidden = elem.nodeType && isHiddenWithinTree( elem ),\n\t\tdataShow = dataPriv.get( elem, \"fxshow\" );\n\n\t// Queue-skipping animations hijack the fx hooks\n\tif ( !opts.queue ) {\n\t\thooks = jQuery._queueHooks( elem, \"fx\" );\n\t\tif ( hooks.unqueued == null ) {\n\t\t\thooks.unqueued = 0;\n\t\t\toldfire = hooks.empty.fire;\n\t\t\thooks.empty.fire = function() {\n\t\t\t\tif ( !hooks.unqueued ) {\n\t\t\t\t\toldfire();\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t\thooks.unqueued++;\n\n\t\tanim.always( function() {\n\n\t\t\t// Ensure the complete handler is called before this completes\n\t\t\tanim.always( function() {\n\t\t\t\thooks.unqueued--;\n\t\t\t\tif ( !jQuery.queue( elem, \"fx\" ).length ) {\n\t\t\t\t\thooks.empty.fire();\n\t\t\t\t}\n\t\t\t} );\n\t\t} );\n\t}\n\n\t// Detect show/hide animations\n\tfor ( prop in props ) {\n\t\tvalue = props[ prop ];\n\t\tif ( rfxtypes.test( value ) ) {\n\t\t\tdelete props[ prop ];\n\t\t\ttoggle = toggle || value === \"toggle\";\n\t\t\tif ( value === ( hidden ? \"hide\" : \"show\" ) ) {\n\n\t\t\t\t// Pretend to be hidden if this is a \"show\" and\n\t\t\t\t// there is still data from a stopped show/hide\n\t\t\t\tif ( value === \"show\" && dataShow && dataShow[ prop ] !== undefined ) {\n\t\t\t\t\thidden = true;\n\n\t\t\t\t// Ignore all other no-op show/hide data\n\t\t\t\t} else {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\torig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );\n\t\t}\n\t}\n\n\t// Bail out if this is a no-op like .hide().hide()\n\tpropTween = !jQuery.isEmptyObject( props );\n\tif ( !propTween && jQuery.isEmptyObject( orig ) ) {\n\t\treturn;\n\t}\n\n\t// Restrict \"overflow\" and \"display\" styles during box animations\n\tif ( isBox && elem.nodeType === 1 ) {\n\n\t\t// Support: IE <=9 - 11, Edge 12 - 15\n\t\t// Record all 3 overflow attributes because IE does not infer the shorthand\n\t\t// from identically-valued overflowX and overflowY and Edge just mirrors\n\t\t// the overflowX value there.\n\t\topts.overflow = [ style.overflow, style.overflowX, style.overflowY ];\n\n\t\t// Identify a display type, preferring old show/hide data over the CSS cascade\n\t\trestoreDisplay = dataShow && dataShow.display;\n\t\tif ( restoreDisplay == null ) {\n\t\t\trestoreDisplay = dataPriv.get( elem, \"display\" );\n\t\t}\n\t\tdisplay = jQuery.css( elem, \"display\" );\n\t\tif ( display === \"none\" ) {\n\t\t\tif ( restoreDisplay ) {\n\t\t\t\tdisplay = restoreDisplay;\n\t\t\t} else {\n\n\t\t\t\t// Get nonempty value(s) by temporarily forcing visibility\n\t\t\t\tshowHide( [ elem ], true );\n\t\t\t\trestoreDisplay = elem.style.display || restoreDisplay;\n\t\t\t\tdisplay = jQuery.css( elem, \"display\" );\n\t\t\t\tshowHide( [ elem ] );\n\t\t\t}\n\t\t}\n\n\t\t// Animate inline elements as inline-block\n\t\tif ( display === \"inline\" || display === \"inline-block\" && restoreDisplay != null ) {\n\t\t\tif ( jQuery.css( elem, \"float\" ) === \"none\" ) {\n\n\t\t\t\t// Restore the original display value at the end of pure show/hide animations\n\t\t\t\tif ( !propTween ) {\n\t\t\t\t\tanim.done( function() {\n\t\t\t\t\t\tstyle.display = restoreDisplay;\n\t\t\t\t\t} );\n\t\t\t\t\tif ( restoreDisplay == null ) {\n\t\t\t\t\t\tdisplay = style.display;\n\t\t\t\t\t\trestoreDisplay = display === \"none\" ? \"\" : display;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstyle.display = \"inline-block\";\n\t\t\t}\n\t\t}\n\t}\n\n\tif ( opts.overflow ) {\n\t\tstyle.overflow = \"hidden\";\n\t\tanim.always( function() {\n\t\t\tstyle.overflow = opts.overflow[ 0 ];\n\t\t\tstyle.overflowX = opts.overflow[ 1 ];\n\t\t\tstyle.overflowY = opts.overflow[ 2 ];\n\t\t} );\n\t}\n\n\t// Implement show/hide animations\n\tpropTween = false;\n\tfor ( prop in orig ) {\n\n\t\t// General show/hide setup for this element animation\n\t\tif ( !propTween ) {\n\t\t\tif ( dataShow ) {\n\t\t\t\tif ( \"hidden\" in dataShow ) {\n\t\t\t\t\thidden = dataShow.hidden;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdataShow = dataPriv.access( elem, \"fxshow\", { display: restoreDisplay } );\n\t\t\t}\n\n\t\t\t// Store hidden/visible for toggle so `.stop().toggle()` \"reverses\"\n\t\t\tif ( toggle ) {\n\t\t\t\tdataShow.hidden = !hidden;\n\t\t\t}\n\n\t\t\t// Show elements before animating them\n\t\t\tif ( hidden ) {\n\t\t\t\tshowHide( [ elem ], true );\n\t\t\t}\n\n\t\t\t/* eslint-disable no-loop-func */\n\n\t\t\tanim.done( function() {\n\n\t\t\t\t/* eslint-enable no-loop-func */\n\n\t\t\t\t// The final step of a \"hide\" animation is actually hiding the element\n\t\t\t\tif ( !hidden ) {\n\t\t\t\t\tshowHide( [ elem ] );\n\t\t\t\t}\n\t\t\t\tdataPriv.remove( elem, \"fxshow\" );\n\t\t\t\tfor ( prop in orig ) {\n\t\t\t\t\tjQuery.style( elem, prop, orig[ prop ] );\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\t// Per-property setup\n\t\tpropTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );\n\t\tif ( !( prop in dataShow ) ) {\n\t\t\tdataShow[ prop ] = propTween.start;\n\t\t\tif ( hidden ) {\n\t\t\t\tpropTween.end = propTween.start;\n\t\t\t\tpropTween.start = 0;\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction propFilter( props, specialEasing ) {\n\tvar index, name, easing, value, hooks;\n\n\t// camelCase, specialEasing and expand cssHook pass\n\tfor ( index in props ) {\n\t\tname = camelCase( index );\n\t\teasing = specialEasing[ name ];\n\t\tvalue = props[ index ];\n\t\tif ( Array.isArray( value ) ) {\n\t\t\teasing = value[ 1 ];\n\t\t\tvalue = props[ index ] = value[ 0 ];\n\t\t}\n\n\t\tif ( index !== name ) {\n\t\t\tprops[ name ] = value;\n\t\t\tdelete props[ index ];\n\t\t}\n\n\t\thooks = jQuery.cssHooks[ name ];\n\t\tif ( hooks && \"expand\" in hooks ) {\n\t\t\tvalue = hooks.expand( value );\n\t\t\tdelete props[ name ];\n\n\t\t\t// Not quite $.extend, this won't overwrite existing keys.\n\t\t\t// Reusing 'index' because we have the correct \"name\"\n\t\t\tfor ( index in value ) {\n\t\t\t\tif ( !( index in props ) ) {\n\t\t\t\t\tprops[ index ] = value[ index ];\n\t\t\t\t\tspecialEasing[ index ] = easing;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tspecialEasing[ name ] = easing;\n\t\t}\n\t}\n}\n\nfunction Animation( elem, properties, options ) {\n\tvar result,\n\t\tstopped,\n\t\tindex = 0,\n\t\tlength = Animation.prefilters.length,\n\t\tdeferred = jQuery.Deferred().always( function() {\n\n\t\t\t// Don't match elem in the :animated selector\n\t\t\tdelete tick.elem;\n\t\t} ),\n\t\ttick = function() {\n\t\t\tif ( stopped ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tvar currentTime = fxNow || createFxNow(),\n\t\t\t\tremaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),\n\n\t\t\t\t// Support: Android 2.3 only\n\t\t\t\t// Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (trac-12497)\n\t\t\t\ttemp = remaining / animation.duration || 0,\n\t\t\t\tpercent = 1 - temp,\n\t\t\t\tindex = 0,\n\t\t\t\tlength = animation.tweens.length;\n\n\t\t\tfor ( ; index < length; index++ ) {\n\t\t\t\tanimation.tweens[ index ].run( percent );\n\t\t\t}\n\n\t\t\tdeferred.notifyWith( elem, [ animation, percent, remaining ] );\n\n\t\t\t// If there's more to do, yield\n\t\t\tif ( percent < 1 && length ) {\n\t\t\t\treturn remaining;\n\t\t\t}\n\n\t\t\t// If this was an empty animation, synthesize a final progress notification\n\t\t\tif ( !length ) {\n\t\t\t\tdeferred.notifyWith( elem, [ animation, 1, 0 ] );\n\t\t\t}\n\n\t\t\t// Resolve the animation and report its conclusion\n\t\t\tdeferred.resolveWith( elem, [ animation ] );\n\t\t\treturn false;\n\t\t},\n\t\tanimation = deferred.promise( {\n\t\t\telem: elem,\n\t\t\tprops: jQuery.extend( {}, properties ),\n\t\t\topts: jQuery.extend( true, {\n\t\t\t\tspecialEasing: {},\n\t\t\t\teasing: jQuery.easing._default\n\t\t\t}, options ),\n\t\t\toriginalProperties: properties,\n\t\t\toriginalOptions: options,\n\t\t\tstartTime: fxNow || createFxNow(),\n\t\t\tduration: options.duration,\n\t\t\ttweens: [],\n\t\t\tcreateTween: function( prop, end ) {\n\t\t\t\tvar tween = jQuery.Tween( elem, animation.opts, prop, end,\n\t\t\t\t\tanimation.opts.specialEasing[ prop ] || animation.opts.easing );\n\t\t\t\tanimation.tweens.push( tween );\n\t\t\t\treturn tween;\n\t\t\t},\n\t\t\tstop: function( gotoEnd ) {\n\t\t\t\tvar index = 0,\n\n\t\t\t\t\t// If we are going to the end, we want to run all the tweens\n\t\t\t\t\t// otherwise we skip this part\n\t\t\t\t\tlength = gotoEnd ? animation.tweens.length : 0;\n\t\t\t\tif ( stopped ) {\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t\tstopped = true;\n\t\t\t\tfor ( ; index < length; index++ ) {\n\t\t\t\t\tanimation.tweens[ index ].run( 1 );\n\t\t\t\t}\n\n\t\t\t\t// Resolve when we played the last frame; otherwise, reject\n\t\t\t\tif ( gotoEnd ) {\n\t\t\t\t\tdeferred.notifyWith( elem, [ animation, 1, 0 ] );\n\t\t\t\t\tdeferred.resolveWith( elem, [ animation, gotoEnd ] );\n\t\t\t\t} else {\n\t\t\t\t\tdeferred.rejectWith( elem, [ animation, gotoEnd ] );\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t}\n\t\t} ),\n\t\tprops = animation.props;\n\n\tpropFilter( props, animation.opts.specialEasing );\n\n\tfor ( ; index < length; index++ ) {\n\t\tresult = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );\n\t\tif ( result ) {\n\t\t\tif ( isFunction( result.stop ) ) {\n\t\t\t\tjQuery._queueHooks( animation.elem, animation.opts.queue ).stop =\n\t\t\t\t\tresult.stop.bind( result );\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\t}\n\n\tjQuery.map( props, createTween, animation );\n\n\tif ( isFunction( animation.opts.start ) ) {\n\t\tanimation.opts.start.call( elem, animation );\n\t}\n\n\t// Attach callbacks from options\n\tanimation\n\t\t.progress( animation.opts.progress )\n\t\t.done( animation.opts.done, animation.opts.complete )\n\t\t.fail( animation.opts.fail )\n\t\t.always( animation.opts.always );\n\n\tjQuery.fx.timer(\n\t\tjQuery.extend( tick, {\n\t\t\telem: elem,\n\t\t\tanim: animation,\n\t\t\tqueue: animation.opts.queue\n\t\t} )\n\t);\n\n\treturn animation;\n}\n\njQuery.Animation = jQuery.extend( Animation, {\n\n\ttweeners: {\n\t\t\"*\": [ function( prop, value ) {\n\t\t\tvar tween = this.createTween( prop, value );\n\t\t\tadjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );\n\t\t\treturn tween;\n\t\t} ]\n\t},\n\n\ttweener: function( props, callback ) {\n\t\tif ( isFunction( props ) ) {\n\t\t\tcallback = props;\n\t\t\tprops = [ \"*\" ];\n\t\t} else {\n\t\t\tprops = props.match( rnothtmlwhite );\n\t\t}\n\n\t\tvar prop,\n\t\t\tindex = 0,\n\t\t\tlength = props.length;\n\n\t\tfor ( ; index < length; index++ ) {\n\t\t\tprop = props[ index ];\n\t\t\tAnimation.tweeners[ prop ] = Animation.tweeners[ prop ] || [];\n\t\t\tAnimation.tweeners[ prop ].unshift( callback );\n\t\t}\n\t},\n\n\tprefilters: [ defaultPrefilter ],\n\n\tprefilter: function( callback, prepend ) {\n\t\tif ( prepend ) {\n\t\t\tAnimation.prefilters.unshift( callback );\n\t\t} else {\n\t\t\tAnimation.prefilters.push( callback );\n\t\t}\n\t}\n} );\n\njQuery.speed = function( speed, easing, fn ) {\n\tvar opt = speed && typeof speed === \"object\" ? jQuery.extend( {}, speed ) : {\n\t\tcomplete: fn || !fn && easing ||\n\t\t\tisFunction( speed ) && speed,\n\t\tduration: speed,\n\t\teasing: fn && easing || easing && !isFunction( easing ) && easing\n\t};\n\n\t// Go to the end state if fx are off\n\tif ( jQuery.fx.off ) {\n\t\topt.duration = 0;\n\n\t} else {\n\t\tif ( typeof opt.duration !== \"number\" ) {\n\t\t\tif ( opt.duration in jQuery.fx.speeds ) {\n\t\t\t\topt.duration = jQuery.fx.speeds[ opt.duration ];\n\n\t\t\t} else {\n\t\t\t\topt.duration = jQuery.fx.speeds._default;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Normalize opt.queue - true/undefined/null -> \"fx\"\n\tif ( opt.queue == null || opt.queue === true ) {\n\t\topt.queue = \"fx\";\n\t}\n\n\t// Queueing\n\topt.old = opt.complete;\n\n\topt.complete = function() {\n\t\tif ( isFunction( opt.old ) ) {\n\t\t\topt.old.call( this );\n\t\t}\n\n\t\tif ( opt.queue ) {\n\t\t\tjQuery.dequeue( this, opt.queue );\n\t\t}\n\t};\n\n\treturn opt;\n};\n\njQuery.fn.extend( {\n\tfadeTo: function( speed, to, easing, callback ) {\n\n\t\t// Show any hidden elements after setting opacity to 0\n\t\treturn this.filter( isHiddenWithinTree ).css( \"opacity\", 0 ).show()\n\n\t\t\t// Animate to the value specified\n\t\t\t.end().animate( { opacity: to }, speed, easing, callback );\n\t},\n\tanimate: function( prop, speed, easing, callback ) {\n\t\tvar empty = jQuery.isEmptyObject( prop ),\n\t\t\toptall = jQuery.speed( speed, easing, callback ),\n\t\t\tdoAnimation = function() {\n\n\t\t\t\t// Operate on a copy of prop so per-property easing won't be lost\n\t\t\t\tvar anim = Animation( this, jQuery.extend( {}, prop ), optall );\n\n\t\t\t\t// Empty animations, or finishing resolves immediately\n\t\t\t\tif ( empty || dataPriv.get( this, \"finish\" ) ) {\n\t\t\t\t\tanim.stop( true );\n\t\t\t\t}\n\t\t\t};\n\n\t\tdoAnimation.finish = doAnimation;\n\n\t\treturn empty || optall.queue === false ?\n\t\t\tthis.each( doAnimation ) :\n\t\t\tthis.queue( optall.queue, doAnimation );\n\t},\n\tstop: function( type, clearQueue, gotoEnd ) {\n\t\tvar stopQueue = function( hooks ) {\n\t\t\tvar stop = hooks.stop;\n\t\t\tdelete hooks.stop;\n\t\t\tstop( gotoEnd );\n\t\t};\n\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tgotoEnd = clearQueue;\n\t\t\tclearQueue = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\tif ( clearQueue ) {\n\t\t\tthis.queue( type || \"fx\", [] );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar dequeue = true,\n\t\t\t\tindex = type != null && type + \"queueHooks\",\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tdata = dataPriv.get( this );\n\n\t\t\tif ( index ) {\n\t\t\t\tif ( data[ index ] && data[ index ].stop ) {\n\t\t\t\t\tstopQueue( data[ index ] );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor ( index in data ) {\n\t\t\t\t\tif ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {\n\t\t\t\t\t\tstopQueue( data[ index ] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this &&\n\t\t\t\t\t( type == null || timers[ index ].queue === type ) ) {\n\n\t\t\t\t\ttimers[ index ].anim.stop( gotoEnd );\n\t\t\t\t\tdequeue = false;\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Start the next in the queue if the last step wasn't forced.\n\t\t\t// Timers currently will call their complete callbacks, which\n\t\t\t// will dequeue but only if they were gotoEnd.\n\t\t\tif ( dequeue || !gotoEnd ) {\n\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t}\n\t\t} );\n\t},\n\tfinish: function( type ) {\n\t\tif ( type !== false ) {\n\t\t\ttype = type || \"fx\";\n\t\t}\n\t\treturn this.each( function() {\n\t\t\tvar index,\n\t\t\t\tdata = dataPriv.get( this ),\n\t\t\t\tqueue = data[ type + \"queue\" ],\n\t\t\t\thooks = data[ type + \"queueHooks\" ],\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tlength = queue ? queue.length : 0;\n\n\t\t\t// Enable finishing flag on private data\n\t\t\tdata.finish = true;\n\n\t\t\t// Empty the queue first\n\t\t\tjQuery.queue( this, type, [] );\n\n\t\t\tif ( hooks && hooks.stop ) {\n\t\t\t\thooks.stop.call( this, true );\n\t\t\t}\n\n\t\t\t// Look for any active animations, and finish them\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this && timers[ index ].queue === type ) {\n\t\t\t\t\ttimers[ index ].anim.stop( true );\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Look for any animations in the old queue and finish them\n\t\t\tfor ( index = 0; index < length; index++ ) {\n\t\t\t\tif ( queue[ index ] && queue[ index ].finish ) {\n\t\t\t\t\tqueue[ index ].finish.call( this );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Turn off finishing flag\n\t\t\tdelete data.finish;\n\t\t} );\n\t}\n} );\n\njQuery.each( [ \"toggle\", \"show\", \"hide\" ], function( _i, name ) {\n\tvar cssFn = jQuery.fn[ name ];\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn speed == null || typeof speed === \"boolean\" ?\n\t\t\tcssFn.apply( this, arguments ) :\n\t\t\tthis.animate( genFx( name, true ), speed, easing, callback );\n\t};\n} );\n\n// Generate shortcuts for custom animations\njQuery.each( {\n\tslideDown: genFx( \"show\" ),\n\tslideUp: genFx( \"hide\" ),\n\tslideToggle: genFx( \"toggle\" ),\n\tfadeIn: { opacity: \"show\" },\n\tfadeOut: { opacity: \"hide\" },\n\tfadeToggle: { opacity: \"toggle\" }\n}, function( name, props ) {\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn this.animate( props, speed, easing, callback );\n\t};\n} );\n\njQuery.timers = [];\njQuery.fx.tick = function() {\n\tvar timer,\n\t\ti = 0,\n\t\ttimers = jQuery.timers;\n\n\tfxNow = Date.now();\n\n\tfor ( ; i < timers.length; i++ ) {\n\t\ttimer = timers[ i ];\n\n\t\t// Run the timer and safely remove it when done (allowing for external removal)\n\t\tif ( !timer() && timers[ i ] === timer ) {\n\t\t\ttimers.splice( i--, 1 );\n\t\t}\n\t}\n\n\tif ( !timers.length ) {\n\t\tjQuery.fx.stop();\n\t}\n\tfxNow = undefined;\n};\n\njQuery.fx.timer = function( timer ) {\n\tjQuery.timers.push( timer );\n\tjQuery.fx.start();\n};\n\njQuery.fx.interval = 13;\njQuery.fx.start = function() {\n\tif ( inProgress ) {\n\t\treturn;\n\t}\n\n\tinProgress = true;\n\tschedule();\n};\n\njQuery.fx.stop = function() {\n\tinProgress = null;\n};\n\njQuery.fx.speeds = {\n\tslow: 600,\n\tfast: 200,\n\n\t// Default speed\n\t_default: 400\n};\n\n\n// Based off of the plugin by Clint Helfers, with permission.\njQuery.fn.delay = function( time, type ) {\n\ttime = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;\n\ttype = type || \"fx\";\n\n\treturn this.queue( type, function( next, hooks ) {\n\t\tvar timeout = window.setTimeout( next, time );\n\t\thooks.stop = function() {\n\t\t\twindow.clearTimeout( timeout );\n\t\t};\n\t} );\n};\n\n\n( function() {\n\tvar input = document.createElement( \"input\" ),\n\t\tselect = document.createElement( \"select\" ),\n\t\topt = select.appendChild( document.createElement( \"option\" ) );\n\n\tinput.type = \"checkbox\";\n\n\t// Support: Android <=4.3 only\n\t// Default value for a checkbox should be \"on\"\n\tsupport.checkOn = input.value !== \"\";\n\n\t// Support: IE <=11 only\n\t// Must access selectedIndex to make default options select\n\tsupport.optSelected = opt.selected;\n\n\t// Support: IE <=11 only\n\t// An input loses its value after becoming a radio\n\tinput = document.createElement( \"input\" );\n\tinput.value = \"t\";\n\tinput.type = \"radio\";\n\tsupport.radioValue = input.value === \"t\";\n} )();\n\n\nvar boolHook,\n\tattrHandle = jQuery.expr.attrHandle;\n\njQuery.fn.extend( {\n\tattr: function( name, value ) {\n\t\treturn access( this, jQuery.attr, name, value, arguments.length > 1 );\n\t},\n\n\tremoveAttr: function( name ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.removeAttr( this, name );\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tattr: function( elem, name, value ) {\n\t\tvar ret, hooks,\n\t\t\tnType = elem.nodeType;\n\n\t\t// Don't get/set attributes on text, comment and attribute nodes\n\t\tif ( nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Fallback to prop when attributes are not supported\n\t\tif ( typeof elem.getAttribute === \"undefined\" ) {\n\t\t\treturn jQuery.prop( elem, name, value );\n\t\t}\n\n\t\t// Attribute hooks are determined by the lowercase version\n\t\t// Grab necessary hook if one is defined\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\t\t\thooks = jQuery.attrHooks[ name.toLowerCase() ] ||\n\t\t\t\t( jQuery.expr.match.bool.test( name ) ? boolHook : undefined );\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\tif ( value === null ) {\n\t\t\t\tjQuery.removeAttr( elem, name );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( hooks && \"set\" in hooks &&\n\t\t\t\t( ret = hooks.set( elem, value, name ) ) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\telem.setAttribute( name, value + \"\" );\n\t\t\treturn value;\n\t\t}\n\n\t\tif ( hooks && \"get\" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {\n\t\t\treturn ret;\n\t\t}\n\n\t\tret = jQuery.find.attr( elem, name );\n\n\t\t// Non-existent attributes return null, we normalize to undefined\n\t\treturn ret == null ? undefined : ret;\n\t},\n\n\tattrHooks: {\n\t\ttype: {\n\t\t\tset: function( elem, value ) {\n\t\t\t\tif ( !support.radioValue && value === \"radio\" &&\n\t\t\t\t\tnodeName( elem, \"input\" ) ) {\n\t\t\t\t\tvar val = elem.value;\n\t\t\t\t\telem.setAttribute( \"type\", value );\n\t\t\t\t\tif ( val ) {\n\t\t\t\t\t\telem.value = val;\n\t\t\t\t\t}\n\t\t\t\t\treturn value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\tremoveAttr: function( elem, value ) {\n\t\tvar name,\n\t\t\ti = 0,\n\n\t\t\t// Attribute names can contain non-HTML whitespace characters\n\t\t\t// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2\n\t\t\tattrNames = value && value.match( rnothtmlwhite );\n\n\t\tif ( attrNames && elem.nodeType === 1 ) {\n\t\t\twhile ( ( name = attrNames[ i++ ] ) ) {\n\t\t\t\telem.removeAttribute( name );\n\t\t\t}\n\t\t}\n\t}\n} );\n\n// Hooks for boolean attributes\nboolHook = {\n\tset: function( elem, value, name ) {\n\t\tif ( value === false ) {\n\n\t\t\t// Remove boolean attributes when set to false\n\t\t\tjQuery.removeAttr( elem, name );\n\t\t} else {\n\t\t\telem.setAttribute( name, name );\n\t\t}\n\t\treturn name;\n\t}\n};\n\njQuery.each( jQuery.expr.match.bool.source.match( /\\w+/g ), function( _i, name ) {\n\tvar getter = attrHandle[ name ] || jQuery.find.attr;\n\n\tattrHandle[ name ] = function( elem, name, isXML ) {\n\t\tvar ret, handle,\n\t\t\tlowercaseName = name.toLowerCase();\n\n\t\tif ( !isXML ) {\n\n\t\t\t// Avoid an infinite loop by temporarily removing this function from the getter\n\t\t\thandle = attrHandle[ lowercaseName ];\n\t\t\tattrHandle[ lowercaseName ] = ret;\n\t\t\tret = getter( elem, name, isXML ) != null ?\n\t\t\t\tlowercaseName :\n\t\t\t\tnull;\n\t\t\tattrHandle[ lowercaseName ] = handle;\n\t\t}\n\t\treturn ret;\n\t};\n} );\n\n\n\n\nvar rfocusable = /^(?:input|select|textarea|button)$/i,\n\trclickable = /^(?:a|area)$/i;\n\njQuery.fn.extend( {\n\tprop: function( name, value ) {\n\t\treturn access( this, jQuery.prop, name, value, arguments.length > 1 );\n\t},\n\n\tremoveProp: function( name ) {\n\t\treturn this.each( function() {\n\t\t\tdelete this[ jQuery.propFix[ name ] || name ];\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tprop: function( elem, name, value ) {\n\t\tvar ret, hooks,\n\t\t\tnType = elem.nodeType;\n\n\t\t// Don't get/set properties on text, comment and attribute nodes\n\t\tif ( nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n\n\t\t\t// Fix name and attach hooks\n\t\t\tname = jQuery.propFix[ name ] || name;\n\t\t\thooks = jQuery.propHooks[ name ];\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\tif ( hooks && \"set\" in hooks &&\n\t\t\t\t( ret = hooks.set( elem, value, name ) ) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\treturn ( elem[ name ] = value );\n\t\t}\n\n\t\tif ( hooks && \"get\" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {\n\t\t\treturn ret;\n\t\t}\n\n\t\treturn elem[ name ];\n\t},\n\n\tpropHooks: {\n\t\ttabIndex: {\n\t\t\tget: function( elem ) {\n\n\t\t\t\t// Support: IE <=9 - 11 only\n\t\t\t\t// elem.tabIndex doesn't always return the\n\t\t\t\t// correct value when it hasn't been explicitly set\n\t\t\t\t// Use proper attribute retrieval (trac-12072)\n\t\t\t\tvar tabindex = jQuery.find.attr( elem, \"tabindex\" );\n\n\t\t\t\tif ( tabindex ) {\n\t\t\t\t\treturn parseInt( tabindex, 10 );\n\t\t\t\t}\n\n\t\t\t\tif (\n\t\t\t\t\trfocusable.test( elem.nodeName ) ||\n\t\t\t\t\trclickable.test( elem.nodeName ) &&\n\t\t\t\t\telem.href\n\t\t\t\t) {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\n\t\t\t\treturn -1;\n\t\t\t}\n\t\t}\n\t},\n\n\tpropFix: {\n\t\t\"for\": \"htmlFor\",\n\t\t\"class\": \"className\"\n\t}\n} );\n\n// Support: IE <=11 only\n// Accessing the selectedIndex property\n// forces the browser to respect setting selected\n// on the option\n// The getter ensures a default option is selected\n// when in an optgroup\n// eslint rule \"no-unused-expressions\" is disabled for this code\n// since it considers such accessions noop\nif ( !support.optSelected ) {\n\tjQuery.propHooks.selected = {\n\t\tget: function( elem ) {\n\n\t\t\t/* eslint no-unused-expressions: \"off\" */\n\n\t\t\tvar parent = elem.parentNode;\n\t\t\tif ( parent && parent.parentNode ) {\n\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t\tset: function( elem ) {\n\n\t\t\t/* eslint no-unused-expressions: \"off\" */\n\n\t\t\tvar parent = elem.parentNode;\n\t\t\tif ( parent ) {\n\t\t\t\tparent.selectedIndex;\n\n\t\t\t\tif ( parent.parentNode ) {\n\t\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n\njQuery.each( [\n\t\"tabIndex\",\n\t\"readOnly\",\n\t\"maxLength\",\n\t\"cellSpacing\",\n\t\"cellPadding\",\n\t\"rowSpan\",\n\t\"colSpan\",\n\t\"useMap\",\n\t\"frameBorder\",\n\t\"contentEditable\"\n], function() {\n\tjQuery.propFix[ this.toLowerCase() ] = this;\n} );\n\n\n\n\n\t// Strip and collapse whitespace according to HTML spec\n\t// https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace\n\tfunction stripAndCollapse( value ) {\n\t\tvar tokens = value.match( rnothtmlwhite ) || [];\n\t\treturn tokens.join( \" \" );\n\t}\n\n\nfunction getClass( elem ) {\n\treturn elem.getAttribute && elem.getAttribute( \"class\" ) || \"\";\n}\n\nfunction classesToArray( value ) {\n\tif ( Array.isArray( value ) ) {\n\t\treturn value;\n\t}\n\tif ( typeof value === \"string\" ) {\n\t\treturn value.match( rnothtmlwhite ) || [];\n\t}\n\treturn [];\n}\n\njQuery.fn.extend( {\n\taddClass: function( value ) {\n\t\tvar classNames, cur, curValue, className, i, finalValue;\n\n\t\tif ( isFunction( value ) ) {\n\t\t\treturn this.each( function( j ) {\n\t\t\t\tjQuery( this ).addClass( value.call( this, j, getClass( this ) ) );\n\t\t\t} );\n\t\t}\n\n\t\tclassNames = classesToArray( value );\n\n\t\tif ( classNames.length ) {\n\t\t\treturn this.each( function() {\n\t\t\t\tcurValue = getClass( this );\n\t\t\t\tcur = this.nodeType === 1 && ( \" \" + stripAndCollapse( curValue ) + \" \" );\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tfor ( i = 0; i < classNames.length; i++ ) {\n\t\t\t\t\t\tclassName = classNames[ i ];\n\t\t\t\t\t\tif ( cur.indexOf( \" \" + className + \" \" ) < 0 ) {\n\t\t\t\t\t\t\tcur += className + \" \";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = stripAndCollapse( cur );\n\t\t\t\t\tif ( curValue !== finalValue ) {\n\t\t\t\t\t\tthis.setAttribute( \"class\", finalValue );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tremoveClass: function( value ) {\n\t\tvar classNames, cur, curValue, className, i, finalValue;\n\n\t\tif ( isFunction( value ) ) {\n\t\t\treturn this.each( function( j ) {\n\t\t\t\tjQuery( this ).removeClass( value.call( this, j, getClass( this ) ) );\n\t\t\t} );\n\t\t}\n\n\t\tif ( !arguments.length ) {\n\t\t\treturn this.attr( \"class\", \"\" );\n\t\t}\n\n\t\tclassNames = classesToArray( value );\n\n\t\tif ( classNames.length ) {\n\t\t\treturn this.each( function() {\n\t\t\t\tcurValue = getClass( this );\n\n\t\t\t\t// This expression is here for better compressibility (see addClass)\n\t\t\t\tcur = this.nodeType === 1 && ( \" \" + stripAndCollapse( curValue ) + \" \" );\n\n\t\t\t\tif ( cur ) {\n\t\t\t\t\tfor ( i = 0; i < classNames.length; i++ ) {\n\t\t\t\t\t\tclassName = classNames[ i ];\n\n\t\t\t\t\t\t// Remove *all* instances\n\t\t\t\t\t\twhile ( cur.indexOf( \" \" + className + \" \" ) > -1 ) {\n\t\t\t\t\t\t\tcur = cur.replace( \" \" + className + \" \", \" \" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Only assign if different to avoid unneeded rendering.\n\t\t\t\t\tfinalValue = stripAndCollapse( cur );\n\t\t\t\t\tif ( curValue !== finalValue ) {\n\t\t\t\t\t\tthis.setAttribute( \"class\", finalValue );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\ttoggleClass: function( value, stateVal ) {\n\t\tvar classNames, className, i, self,\n\t\t\ttype = typeof value,\n\t\t\tisValidValue = type === \"string\" || Array.isArray( value );\n\n\t\tif ( isFunction( value ) ) {\n\t\t\treturn this.each( function( i ) {\n\t\t\t\tjQuery( this ).toggleClass(\n\t\t\t\t\tvalue.call( this, i, getClass( this ), stateVal ),\n\t\t\t\t\tstateVal\n\t\t\t\t);\n\t\t\t} );\n\t\t}\n\n\t\tif ( typeof stateVal === \"boolean\" && isValidValue ) {\n\t\t\treturn stateVal ? this.addClass( value ) : this.removeClass( value );\n\t\t}\n\n\t\tclassNames = classesToArray( value );\n\n\t\treturn this.each( function() {\n\t\t\tif ( isValidValue ) {\n\n\t\t\t\t// Toggle individual class names\n\t\t\t\tself = jQuery( this );\n\n\t\t\t\tfor ( i = 0; i < classNames.length; i++ ) {\n\t\t\t\t\tclassName = classNames[ i ];\n\n\t\t\t\t\t// Check each className given, space separated list\n\t\t\t\t\tif ( self.hasClass( className ) ) {\n\t\t\t\t\t\tself.removeClass( className );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.addClass( className );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t// Toggle whole class name\n\t\t\t} else if ( value === undefined || type === \"boolean\" ) {\n\t\t\t\tclassName = getClass( this );\n\t\t\t\tif ( className ) {\n\n\t\t\t\t\t// Store className if set\n\t\t\t\t\tdataPriv.set( this, \"__className__\", className );\n\t\t\t\t}\n\n\t\t\t\t// If the element has a class name or if we're passed `false`,\n\t\t\t\t// then remove the whole classname (if there was one, the above saved it).\n\t\t\t\t// Otherwise bring back whatever was previously saved (if anything),\n\t\t\t\t// falling back to the empty string if nothing was stored.\n\t\t\t\tif ( this.setAttribute ) {\n\t\t\t\t\tthis.setAttribute( \"class\",\n\t\t\t\t\t\tclassName || value === false ?\n\t\t\t\t\t\t\t\"\" :\n\t\t\t\t\t\t\tdataPriv.get( this, \"__className__\" ) || \"\"\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t},\n\n\thasClass: function( selector ) {\n\t\tvar className, elem,\n\t\t\ti = 0;\n\n\t\tclassName = \" \" + selector + \" \";\n\t\twhile ( ( elem = this[ i++ ] ) ) {\n\t\t\tif ( elem.nodeType === 1 &&\n\t\t\t\t( \" \" + stripAndCollapse( getClass( elem ) ) + \" \" ).indexOf( className ) > -1 ) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n} );\n\n\n\n\nvar rreturn = /\\r/g;\n\njQuery.fn.extend( {\n\tval: function( value ) {\n\t\tvar hooks, ret, valueIsFunction,\n\t\t\telem = this[ 0 ];\n\n\t\tif ( !arguments.length ) {\n\t\t\tif ( elem ) {\n\t\t\t\thooks = jQuery.valHooks[ elem.type ] ||\n\t\t\t\t\tjQuery.valHooks[ elem.nodeName.toLowerCase() ];\n\n\t\t\t\tif ( hooks &&\n\t\t\t\t\t\"get\" in hooks &&\n\t\t\t\t\t( ret = hooks.get( elem, \"value\" ) ) !== undefined\n\t\t\t\t) {\n\t\t\t\t\treturn ret;\n\t\t\t\t}\n\n\t\t\t\tret = elem.value;\n\n\t\t\t\t// Handle most common string cases\n\t\t\t\tif ( typeof ret === \"string\" ) {\n\t\t\t\t\treturn ret.replace( rreturn, \"\" );\n\t\t\t\t}\n\n\t\t\t\t// Handle cases where value is null/undef or number\n\t\t\t\treturn ret == null ? \"\" : ret;\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tvalueIsFunction = isFunction( value );\n\n\t\treturn this.each( function( i ) {\n\t\t\tvar val;\n\n\t\t\tif ( this.nodeType !== 1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( valueIsFunction ) {\n\t\t\t\tval = value.call( this, i, jQuery( this ).val() );\n\t\t\t} else {\n\t\t\t\tval = value;\n\t\t\t}\n\n\t\t\t// Treat null/undefined as \"\"; convert numbers to string\n\t\t\tif ( val == null ) {\n\t\t\t\tval = \"\";\n\n\t\t\t} else if ( typeof val === \"number\" ) {\n\t\t\t\tval += \"\";\n\n\t\t\t} else if ( Array.isArray( val ) ) {\n\t\t\t\tval = jQuery.map( val, function( value ) {\n\t\t\t\t\treturn value == null ? \"\" : value + \"\";\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\thooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];\n\n\t\t\t// If set returns undefined, fall back to normal setting\n\t\t\tif ( !hooks || !( \"set\" in hooks ) || hooks.set( this, val, \"value\" ) === undefined ) {\n\t\t\t\tthis.value = val;\n\t\t\t}\n\t\t} );\n\t}\n} );\n\njQuery.extend( {\n\tvalHooks: {\n\t\toption: {\n\t\t\tget: function( elem ) {\n\n\t\t\t\tvar val = jQuery.find.attr( elem, \"value\" );\n\t\t\t\treturn val != null ?\n\t\t\t\t\tval :\n\n\t\t\t\t\t// Support: IE <=10 - 11 only\n\t\t\t\t\t// option.text throws exceptions (trac-14686, trac-14858)\n\t\t\t\t\t// Strip and collapse whitespace\n\t\t\t\t\t// https://html.spec.whatwg.org/#strip-and-collapse-whitespace\n\t\t\t\t\tstripAndCollapse( jQuery.text( elem ) );\n\t\t\t}\n\t\t},\n\t\tselect: {\n\t\t\tget: function( elem ) {\n\t\t\t\tvar value, option, i,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tindex = elem.selectedIndex,\n\t\t\t\t\tone = elem.type === \"select-one\",\n\t\t\t\t\tvalues = one ? null : [],\n\t\t\t\t\tmax = one ? index + 1 : options.length;\n\n\t\t\t\tif ( index < 0 ) {\n\t\t\t\t\ti = max;\n\n\t\t\t\t} else {\n\t\t\t\t\ti = one ? index : 0;\n\t\t\t\t}\n\n\t\t\t\t// Loop through all the selected options\n\t\t\t\tfor ( ; i < max; i++ ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t// IE8-9 doesn't update selected after form reset (trac-2551)\n\t\t\t\t\tif ( ( option.selected || i === index ) &&\n\n\t\t\t\t\t\t\t// Don't return options that are disabled or in a disabled optgroup\n\t\t\t\t\t\t\t!option.disabled &&\n\t\t\t\t\t\t\t( !option.parentNode.disabled ||\n\t\t\t\t\t\t\t\t!nodeName( option.parentNode, \"optgroup\" ) ) ) {\n\n\t\t\t\t\t\t// Get the specific value for the option\n\t\t\t\t\t\tvalue = jQuery( option ).val();\n\n\t\t\t\t\t\t// We don't need an array for one selects\n\t\t\t\t\t\tif ( one ) {\n\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Multi-Selects return an array\n\t\t\t\t\t\tvalues.push( value );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn values;\n\t\t\t},\n\n\t\t\tset: function( elem, value ) {\n\t\t\t\tvar optionSet, option,\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tvalues = jQuery.makeArray( value ),\n\t\t\t\t\ti = options.length;\n\n\t\t\t\twhile ( i-- ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\t/* eslint-disable no-cond-assign */\n\n\t\t\t\t\tif ( option.selected =\n\t\t\t\t\t\tjQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1\n\t\t\t\t\t) {\n\t\t\t\t\t\toptionSet = true;\n\t\t\t\t\t}\n\n\t\t\t\t\t/* eslint-enable no-cond-assign */\n\t\t\t\t}\n\n\t\t\t\t// Force browsers to behave consistently when non-matching value is set\n\t\t\t\tif ( !optionSet ) {\n\t\t\t\t\telem.selectedIndex = -1;\n\t\t\t\t}\n\t\t\t\treturn values;\n\t\t\t}\n\t\t}\n\t}\n} );\n\n// Radios and checkboxes getter/setter\njQuery.each( [ \"radio\", \"checkbox\" ], function() {\n\tjQuery.valHooks[ this ] = {\n\t\tset: function( elem, value ) {\n\t\t\tif ( Array.isArray( value ) ) {\n\t\t\t\treturn ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 );\n\t\t\t}\n\t\t}\n\t};\n\tif ( !support.checkOn ) {\n\t\tjQuery.valHooks[ this ].get = function( elem ) {\n\t\t\treturn elem.getAttribute( \"value\" ) === null ? \"on\" : elem.value;\n\t\t};\n\t}\n} );\n\n\n\n\n// Return jQuery for attributes-only inclusion\n\n\nsupport.focusin = \"onfocusin\" in window;\n\n\nvar rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,\n\tstopPropagationCallback = function( e ) {\n\t\te.stopPropagation();\n\t};\n\njQuery.extend( jQuery.event, {\n\n\ttrigger: function( event, data, elem, onlyHandlers ) {\n\n\t\tvar i, cur, tmp, bubbleType, ontype, handle, special, lastElement,\n\t\t\teventPath = [ elem || document ],\n\t\t\ttype = hasOwn.call( event, \"type\" ) ? event.type : event,\n\t\t\tnamespaces = hasOwn.call( event, \"namespace\" ) ? event.namespace.split( \".\" ) : [];\n\n\t\tcur = lastElement = tmp = elem = elem || document;\n\n\t\t// Don't do events on text and comment nodes\n\t\tif ( elem.nodeType === 3 || elem.nodeType === 8 ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// focus/blur morphs to focusin/out; ensure we're not firing them right now\n\t\tif ( rfocusMorph.test( type + jQuery.event.triggered ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( type.indexOf( \".\" ) > -1 ) {\n\n\t\t\t// Namespaced trigger; create a regexp to match event type in handle()\n\t\t\tnamespaces = type.split( \".\" );\n\t\t\ttype = namespaces.shift();\n\t\t\tnamespaces.sort();\n\t\t}\n\t\tontype = type.indexOf( \":\" ) < 0 && \"on\" + type;\n\n\t\t// Caller can pass in a jQuery.Event object, Object, or just an event type string\n\t\tevent = event[ jQuery.expando ] ?\n\t\t\tevent :\n\t\t\tnew jQuery.Event( type, typeof event === \"object\" && event );\n\n\t\t// Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)\n\t\tevent.isTrigger = onlyHandlers ? 2 : 3;\n\t\tevent.namespace = namespaces.join( \".\" );\n\t\tevent.rnamespace = event.namespace ?\n\t\t\tnew RegExp( \"(^|\\\\.)\" + namespaces.join( \"\\\\.(?:.*\\\\.|)\" ) + \"(\\\\.|$)\" ) :\n\t\t\tnull;\n\n\t\t// Clean up the event in case it is being reused\n\t\tevent.result = undefined;\n\t\tif ( !event.target ) {\n\t\t\tevent.target = elem;\n\t\t}\n\n\t\t// Clone any incoming data and prepend the event, creating the handler arg list\n\t\tdata = data == null ?\n\t\t\t[ event ] :\n\t\t\tjQuery.makeArray( data, [ event ] );\n\n\t\t// Allow special events to draw outside the lines\n\t\tspecial = jQuery.event.special[ type ] || {};\n\t\tif ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine event propagation path in advance, per W3C events spec (trac-9951)\n\t\t// Bubble up to document, then to window; watch for a global ownerDocument var (trac-9724)\n\t\tif ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) {\n\n\t\t\tbubbleType = special.delegateType || type;\n\t\t\tif ( !rfocusMorph.test( bubbleType + type ) ) {\n\t\t\t\tcur = cur.parentNode;\n\t\t\t}\n\t\t\tfor ( ; cur; cur = cur.parentNode ) {\n\t\t\t\teventPath.push( cur );\n\t\t\t\ttmp = cur;\n\t\t\t}\n\n\t\t\t// Only add window if we got to document (e.g., not plain obj or detached DOM)\n\t\t\tif ( tmp === ( elem.ownerDocument || document ) ) {\n\t\t\t\teventPath.push( tmp.defaultView || tmp.parentWindow || window );\n\t\t\t}\n\t\t}\n\n\t\t// Fire handlers on the event path\n\t\ti = 0;\n\t\twhile ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {\n\t\t\tlastElement = cur;\n\t\t\tevent.type = i > 1 ?\n\t\t\t\tbubbleType :\n\t\t\t\tspecial.bindType || type;\n\n\t\t\t// jQuery handler\n\t\t\thandle = ( dataPriv.get( cur, \"events\" ) || Object.create( null ) )[ event.type ] &&\n\t\t\t\tdataPriv.get( cur, \"handle\" );\n\t\t\tif ( handle ) {\n\t\t\t\thandle.apply( cur, data );\n\t\t\t}\n\n\t\t\t// Native handler\n\t\t\thandle = ontype && cur[ ontype ];\n\t\t\tif ( handle && handle.apply && acceptData( cur ) ) {\n\t\t\t\tevent.result = handle.apply( cur, data );\n\t\t\t\tif ( event.result === false ) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tevent.type = type;\n\n\t\t// If nobody prevented the default action, do it now\n\t\tif ( !onlyHandlers && !event.isDefaultPrevented() ) {\n\n\t\t\tif ( ( !special._default ||\n\t\t\t\tspecial._default.apply( eventPath.pop(), data ) === false ) &&\n\t\t\t\tacceptData( elem ) ) {\n\n\t\t\t\t// Call a native DOM method on the target with the same name as the event.\n\t\t\t\t// Don't do default actions on window, that's where global variables be (trac-6170)\n\t\t\t\tif ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) {\n\n\t\t\t\t\t// Don't re-trigger an onFOO event when we call its FOO() method\n\t\t\t\t\ttmp = elem[ ontype ];\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = null;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prevent re-triggering of the same event, since we already bubbled it above\n\t\t\t\t\tjQuery.event.triggered = type;\n\n\t\t\t\t\tif ( event.isPropagationStopped() ) {\n\t\t\t\t\t\tlastElement.addEventListener( type, stopPropagationCallback );\n\t\t\t\t\t}\n\n\t\t\t\t\telem[ type ]();\n\n\t\t\t\t\tif ( event.isPropagationStopped() ) {\n\t\t\t\t\t\tlastElement.removeEventListener( type, stopPropagationCallback );\n\t\t\t\t\t}\n\n\t\t\t\t\tjQuery.event.triggered = undefined;\n\n\t\t\t\t\tif ( tmp ) {\n\t\t\t\t\t\telem[ ontype ] = tmp;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\t// Piggyback on a donor event to simulate a different one\n\t// Used only for `focus(in | out)` events\n\tsimulate: function( type, elem, event ) {\n\t\tvar e = jQuery.extend(\n\t\t\tnew jQuery.Event(),\n\t\t\tevent,\n\t\t\t{\n\t\t\t\ttype: type,\n\t\t\t\tisSimulated: true\n\t\t\t}\n\t\t);\n\n\t\tjQuery.event.trigger( e, null, elem );\n\t}\n\n} );\n\njQuery.fn.extend( {\n\n\ttrigger: function( type, data ) {\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.trigger( type, data, this );\n\t\t} );\n\t},\n\ttriggerHandler: function( type, data ) {\n\t\tvar elem = this[ 0 ];\n\t\tif ( elem ) {\n\t\t\treturn jQuery.event.trigger( type, data, elem, true );\n\t\t}\n\t}\n} );\n\n\n// Support: Firefox <=44\n// Firefox doesn't have focus(in | out) events\n// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787\n//\n// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1\n// focus(in | out) events fire after focus & blur events,\n// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order\n// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857\nif ( !support.focusin ) {\n\tjQuery.each( { focus: \"focusin\", blur: \"focusout\" }, function( orig, fix ) {\n\n\t\t// Attach a single capturing handler on the document while someone wants focusin/focusout\n\t\tvar handler = function( event ) {\n\t\t\tjQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) );\n\t\t};\n\n\t\tjQuery.event.special[ fix ] = {\n\t\t\tsetup: function() {\n\n\t\t\t\t// Handle: regular nodes (via `this.ownerDocument`), window\n\t\t\t\t// (via `this.document`) & document (via `this`).\n\t\t\t\tvar doc = this.ownerDocument || this.document || this,\n\t\t\t\t\tattaches = dataPriv.access( doc, fix );\n\n\t\t\t\tif ( !attaches ) {\n\t\t\t\t\tdoc.addEventListener( orig, handler, true );\n\t\t\t\t}\n\t\t\t\tdataPriv.access( doc, fix, ( attaches || 0 ) + 1 );\n\t\t\t},\n\t\t\tteardown: function() {\n\t\t\t\tvar doc = this.ownerDocument || this.document || this,\n\t\t\t\t\tattaches = dataPriv.access( doc, fix ) - 1;\n\n\t\t\t\tif ( !attaches ) {\n\t\t\t\t\tdoc.removeEventListener( orig, handler, true );\n\t\t\t\t\tdataPriv.remove( doc, fix );\n\n\t\t\t\t} else {\n\t\t\t\t\tdataPriv.access( doc, fix, attaches );\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t} );\n}\nvar location = window.location;\n\nvar nonce = { guid: Date.now() };\n\nvar rquery = ( /\\?/ );\n\n\n\n// Cross-browser xml parsing\njQuery.parseXML = function( data ) {\n\tvar xml, parserErrorElem;\n\tif ( !data || typeof data !== \"string\" ) {\n\t\treturn null;\n\t}\n\n\t// Support: IE 9 - 11 only\n\t// IE throws on parseFromString with invalid input.\n\ttry {\n\t\txml = ( new window.DOMParser() ).parseFromString( data, \"text/xml\" );\n\t} catch ( e ) {}\n\n\tparserErrorElem = xml && xml.getElementsByTagName( \"parsererror\" )[ 0 ];\n\tif ( !xml || parserErrorElem ) {\n\t\tjQuery.error( \"Invalid XML: \" + (\n\t\t\tparserErrorElem ?\n\t\t\t\tjQuery.map( parserErrorElem.childNodes, function( el ) {\n\t\t\t\t\treturn el.textContent;\n\t\t\t\t} ).join( \"\\n\" ) :\n\t\t\t\tdata\n\t\t) );\n\t}\n\treturn xml;\n};\n\n\nvar\n\trbracket = /\\[\\]$/,\n\trCRLF = /\\r?\\n/g,\n\trsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,\n\trsubmittable = /^(?:input|select|textarea|keygen)/i;\n\nfunction buildParams( prefix, obj, traditional, add ) {\n\tvar name;\n\n\tif ( Array.isArray( obj ) ) {\n\n\t\t// Serialize array item.\n\t\tjQuery.each( obj, function( i, v ) {\n\t\t\tif ( traditional || rbracket.test( prefix ) ) {\n\n\t\t\t\t// Treat each array item as a scalar.\n\t\t\t\tadd( prefix, v );\n\n\t\t\t} else {\n\n\t\t\t\t// Item is non-scalar (array or object), encode its numeric index.\n\t\t\t\tbuildParams(\n\t\t\t\t\tprefix + \"[\" + ( typeof v === \"object\" && v != null ? i : \"\" ) + \"]\",\n\t\t\t\t\tv,\n\t\t\t\t\ttraditional,\n\t\t\t\t\tadd\n\t\t\t\t);\n\t\t\t}\n\t\t} );\n\n\t} else if ( !traditional && toType( obj ) === \"object\" ) {\n\n\t\t// Serialize object item.\n\t\tfor ( name in obj ) {\n\t\t\tbuildParams( prefix + \"[\" + name + \"]\", obj[ name ], traditional, add );\n\t\t}\n\n\t} else {\n\n\t\t// Serialize scalar item.\n\t\tadd( prefix, obj );\n\t}\n}\n\n// Serialize an array of form elements or a set of\n// key/values into a query string\njQuery.param = function( a, traditional ) {\n\tvar prefix,\n\t\ts = [],\n\t\tadd = function( key, valueOrFunction ) {\n\n\t\t\t// If value is a function, invoke it and use its return value\n\t\t\tvar value = isFunction( valueOrFunction ) ?\n\t\t\t\tvalueOrFunction() :\n\t\t\t\tvalueOrFunction;\n\n\t\t\ts[ s.length ] = encodeURIComponent( key ) + \"=\" +\n\t\t\t\tencodeURIComponent( value == null ? \"\" : value );\n\t\t};\n\n\tif ( a == null ) {\n\t\treturn \"\";\n\t}\n\n\t// If an array was passed in, assume that it is an array of form elements.\n\tif ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {\n\n\t\t// Serialize the form elements\n\t\tjQuery.each( a, function() {\n\t\t\tadd( this.name, this.value );\n\t\t} );\n\n\t} else {\n\n\t\t// If traditional, encode the \"old\" way (the way 1.3.2 or older\n\t\t// did it), otherwise encode params recursively.\n\t\tfor ( prefix in a ) {\n\t\t\tbuildParams( prefix, a[ prefix ], traditional, add );\n\t\t}\n\t}\n\n\t// Return the resulting serialization\n\treturn s.join( \"&\" );\n};\n\njQuery.fn.extend( {\n\tserialize: function() {\n\t\treturn jQuery.param( this.serializeArray() );\n\t},\n\tserializeArray: function() {\n\t\treturn this.map( function() {\n\n\t\t\t// Can add propHook for \"elements\" to filter or add form elements\n\t\t\tvar elements = jQuery.prop( this, \"elements\" );\n\t\t\treturn elements ? jQuery.makeArray( elements ) : this;\n\t\t} ).filter( function() {\n\t\t\tvar type = this.type;\n\n\t\t\t// Use .is( \":disabled\" ) so that fieldset[disabled] works\n\t\t\treturn this.name && !jQuery( this ).is( \":disabled\" ) &&\n\t\t\t\trsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&\n\t\t\t\t( this.checked || !rcheckableType.test( type ) );\n\t\t} ).map( function( _i, elem ) {\n\t\t\tvar val = jQuery( this ).val();\n\n\t\t\tif ( val == null ) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tif ( Array.isArray( val ) ) {\n\t\t\t\treturn jQuery.map( val, function( val ) {\n\t\t\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t} ).get();\n\t}\n} );\n\n\nvar\n\tr20 = /%20/g,\n\trhash = /#.*$/,\n\trantiCache = /([?&])_=[^&]*/,\n\trheaders = /^(.*?):[ \\t]*([^\\r\\n]*)$/mg,\n\n\t// trac-7653, trac-8125, trac-8152: local protocol detection\n\trlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,\n\trnoContent = /^(?:GET|HEAD)$/,\n\trprotocol = /^\\/\\//,\n\n\t/* Prefilters\n\t * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)\n\t * 2) These are called:\n\t *    - BEFORE asking for a transport\n\t *    - AFTER param serialization (s.data is a string if s.processData is true)\n\t * 3) key is the dataType\n\t * 4) the catchall symbol \"*\" can be used\n\t * 5) execution will start with transport dataType and THEN continue down to \"*\" if needed\n\t */\n\tprefilters = {},\n\n\t/* Transports bindings\n\t * 1) key is the dataType\n\t * 2) the catchall symbol \"*\" can be used\n\t * 3) selection will start with transport dataType and THEN go to \"*\" if needed\n\t */\n\ttransports = {},\n\n\t// Avoid comment-prolog char sequence (trac-10098); must appease lint and evade compression\n\tallTypes = \"*/\".concat( \"*\" ),\n\n\t// Anchor tag for parsing the document origin\n\toriginAnchor = document.createElement( \"a\" );\n\noriginAnchor.href = location.href;\n\n// Base \"constructor\" for jQuery.ajaxPrefilter and jQuery.ajaxTransport\nfunction addToPrefiltersOrTransports( structure ) {\n\n\t// dataTypeExpression is optional and defaults to \"*\"\n\treturn function( dataTypeExpression, func ) {\n\n\t\tif ( typeof dataTypeExpression !== \"string\" ) {\n\t\t\tfunc = dataTypeExpression;\n\t\t\tdataTypeExpression = \"*\";\n\t\t}\n\n\t\tvar dataType,\n\t\t\ti = 0,\n\t\t\tdataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || [];\n\n\t\tif ( isFunction( func ) ) {\n\n\t\t\t// For each dataType in the dataTypeExpression\n\t\t\twhile ( ( dataType = dataTypes[ i++ ] ) ) {\n\n\t\t\t\t// Prepend if requested\n\t\t\t\tif ( dataType[ 0 ] === \"+\" ) {\n\t\t\t\t\tdataType = dataType.slice( 1 ) || \"*\";\n\t\t\t\t\t( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func );\n\n\t\t\t\t// Otherwise append\n\t\t\t\t} else {\n\t\t\t\t\t( structure[ dataType ] = structure[ dataType ] || [] ).push( func );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n\n// Base inspection function for prefilters and transports\nfunction inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {\n\n\tvar inspected = {},\n\t\tseekingTransport = ( structure === transports );\n\n\tfunction inspect( dataType ) {\n\t\tvar selected;\n\t\tinspected[ dataType ] = true;\n\t\tjQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {\n\t\t\tvar dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );\n\t\t\tif ( typeof dataTypeOrTransport === \"string\" &&\n\t\t\t\t!seekingTransport && !inspected[ dataTypeOrTransport ] ) {\n\n\t\t\t\toptions.dataTypes.unshift( dataTypeOrTransport );\n\t\t\t\tinspect( dataTypeOrTransport );\n\t\t\t\treturn false;\n\t\t\t} else if ( seekingTransport ) {\n\t\t\t\treturn !( selected = dataTypeOrTransport );\n\t\t\t}\n\t\t} );\n\t\treturn selected;\n\t}\n\n\treturn inspect( options.dataTypes[ 0 ] ) || !inspected[ \"*\" ] && inspect( \"*\" );\n}\n\n// A special extend for ajax options\n// that takes \"flat\" options (not to be deep extended)\n// Fixes trac-9887\nfunction ajaxExtend( target, src ) {\n\tvar key, deep,\n\t\tflatOptions = jQuery.ajaxSettings.flatOptions || {};\n\n\tfor ( key in src ) {\n\t\tif ( src[ key ] !== undefined ) {\n\t\t\t( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];\n\t\t}\n\t}\n\tif ( deep ) {\n\t\tjQuery.extend( true, target, deep );\n\t}\n\n\treturn target;\n}\n\n/* Handles responses to an ajax request:\n * - finds the right dataType (mediates between content-type and expected dataType)\n * - returns the corresponding response\n */\nfunction ajaxHandleResponses( s, jqXHR, responses ) {\n\n\tvar ct, type, finalDataType, firstDataType,\n\t\tcontents = s.contents,\n\t\tdataTypes = s.dataTypes;\n\n\t// Remove auto dataType and get content-type in the process\n\twhile ( dataTypes[ 0 ] === \"*\" ) {\n\t\tdataTypes.shift();\n\t\tif ( ct === undefined ) {\n\t\t\tct = s.mimeType || jqXHR.getResponseHeader( \"Content-Type\" );\n\t\t}\n\t}\n\n\t// Check if we're dealing with a known content-type\n\tif ( ct ) {\n\t\tfor ( type in contents ) {\n\t\t\tif ( contents[ type ] && contents[ type ].test( ct ) ) {\n\t\t\t\tdataTypes.unshift( type );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check to see if we have a response for the expected dataType\n\tif ( dataTypes[ 0 ] in responses ) {\n\t\tfinalDataType = dataTypes[ 0 ];\n\t} else {\n\n\t\t// Try convertible dataTypes\n\t\tfor ( type in responses ) {\n\t\t\tif ( !dataTypes[ 0 ] || s.converters[ type + \" \" + dataTypes[ 0 ] ] ) {\n\t\t\t\tfinalDataType = type;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( !firstDataType ) {\n\t\t\t\tfirstDataType = type;\n\t\t\t}\n\t\t}\n\n\t\t// Or just use first one\n\t\tfinalDataType = finalDataType || firstDataType;\n\t}\n\n\t// If we found a dataType\n\t// We add the dataType to the list if needed\n\t// and return the corresponding response\n\tif ( finalDataType ) {\n\t\tif ( finalDataType !== dataTypes[ 0 ] ) {\n\t\t\tdataTypes.unshift( finalDataType );\n\t\t}\n\t\treturn responses[ finalDataType ];\n\t}\n}\n\n/* Chain conversions given the request and the original response\n * Also sets the responseXXX fields on the jqXHR instance\n */\nfunction ajaxConvert( s, response, jqXHR, isSuccess ) {\n\tvar conv2, current, conv, tmp, prev,\n\t\tconverters = {},\n\n\t\t// Work with a copy of dataTypes in case we need to modify it for conversion\n\t\tdataTypes = s.dataTypes.slice();\n\n\t// Create converters map with lowercased keys\n\tif ( dataTypes[ 1 ] ) {\n\t\tfor ( conv in s.converters ) {\n\t\t\tconverters[ conv.toLowerCase() ] = s.converters[ conv ];\n\t\t}\n\t}\n\n\tcurrent = dataTypes.shift();\n\n\t// Convert to each sequential dataType\n\twhile ( current ) {\n\n\t\tif ( s.responseFields[ current ] ) {\n\t\t\tjqXHR[ s.responseFields[ current ] ] = response;\n\t\t}\n\n\t\t// Apply the dataFilter if provided\n\t\tif ( !prev && isSuccess && s.dataFilter ) {\n\t\t\tresponse = s.dataFilter( response, s.dataType );\n\t\t}\n\n\t\tprev = current;\n\t\tcurrent = dataTypes.shift();\n\n\t\tif ( current ) {\n\n\t\t\t// There's only work to do if current dataType is non-auto\n\t\t\tif ( current === \"*\" ) {\n\n\t\t\t\tcurrent = prev;\n\n\t\t\t// Convert response if prev dataType is non-auto and differs from current\n\t\t\t} else if ( prev !== \"*\" && prev !== current ) {\n\n\t\t\t\t// Seek a direct converter\n\t\t\t\tconv = converters[ prev + \" \" + current ] || converters[ \"* \" + current ];\n\n\t\t\t\t// If none found, seek a pair\n\t\t\t\tif ( !conv ) {\n\t\t\t\t\tfor ( conv2 in converters ) {\n\n\t\t\t\t\t\t// If conv2 outputs current\n\t\t\t\t\t\ttmp = conv2.split( \" \" );\n\t\t\t\t\t\tif ( tmp[ 1 ] === current ) {\n\n\t\t\t\t\t\t\t// If prev can be converted to accepted input\n\t\t\t\t\t\t\tconv = converters[ prev + \" \" + tmp[ 0 ] ] ||\n\t\t\t\t\t\t\t\tconverters[ \"* \" + tmp[ 0 ] ];\n\t\t\t\t\t\t\tif ( conv ) {\n\n\t\t\t\t\t\t\t\t// Condense equivalence converters\n\t\t\t\t\t\t\t\tif ( conv === true ) {\n\t\t\t\t\t\t\t\t\tconv = converters[ conv2 ];\n\n\t\t\t\t\t\t\t\t// Otherwise, insert the intermediate dataType\n\t\t\t\t\t\t\t\t} else if ( converters[ conv2 ] !== true ) {\n\t\t\t\t\t\t\t\t\tcurrent = tmp[ 0 ];\n\t\t\t\t\t\t\t\t\tdataTypes.unshift( tmp[ 1 ] );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Apply converter (if not an equivalence)\n\t\t\t\tif ( conv !== true ) {\n\n\t\t\t\t\t// Unless errors are allowed to bubble, catch and return them\n\t\t\t\t\tif ( conv && s.throws ) {\n\t\t\t\t\t\tresponse = conv( response );\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tresponse = conv( response );\n\t\t\t\t\t\t} catch ( e ) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tstate: \"parsererror\",\n\t\t\t\t\t\t\t\terror: conv ? e : \"No conversion from \" + prev + \" to \" + current\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { state: \"success\", data: response };\n}\n\njQuery.extend( {\n\n\t// Counter for holding the number of active queries\n\tactive: 0,\n\n\t// Last-Modified header cache for next request\n\tlastModified: {},\n\tetag: {},\n\n\tajaxSettings: {\n\t\turl: location.href,\n\t\ttype: \"GET\",\n\t\tisLocal: rlocalProtocol.test( location.protocol ),\n\t\tglobal: true,\n\t\tprocessData: true,\n\t\tasync: true,\n\t\tcontentType: \"application/x-www-form-urlencoded; charset=UTF-8\",\n\n\t\t/*\n\t\ttimeout: 0,\n\t\tdata: null,\n\t\tdataType: null,\n\t\tusername: null,\n\t\tpassword: null,\n\t\tcache: null,\n\t\tthrows: false,\n\t\ttraditional: false,\n\t\theaders: {},\n\t\t*/\n\n\t\taccepts: {\n\t\t\t\"*\": allTypes,\n\t\t\ttext: \"text/plain\",\n\t\t\thtml: \"text/html\",\n\t\t\txml: \"application/xml, text/xml\",\n\t\t\tjson: \"application/json, text/javascript\"\n\t\t},\n\n\t\tcontents: {\n\t\t\txml: /\\bxml\\b/,\n\t\t\thtml: /\\bhtml/,\n\t\t\tjson: /\\bjson\\b/\n\t\t},\n\n\t\tresponseFields: {\n\t\t\txml: \"responseXML\",\n\t\t\ttext: \"responseText\",\n\t\t\tjson: \"responseJSON\"\n\t\t},\n\n\t\t// Data converters\n\t\t// Keys separate source (or catchall \"*\") and destination types with a single space\n\t\tconverters: {\n\n\t\t\t// Convert anything to text\n\t\t\t\"* text\": String,\n\n\t\t\t// Text to html (true = no transformation)\n\t\t\t\"text html\": true,\n\n\t\t\t// Evaluate text as a json expression\n\t\t\t\"text json\": JSON.parse,\n\n\t\t\t// Parse text as xml\n\t\t\t\"text xml\": jQuery.parseXML\n\t\t},\n\n\t\t// For options that shouldn't be deep extended:\n\t\t// you can add your own custom options here if\n\t\t// and when you create one that shouldn't be\n\t\t// deep extended (see ajaxExtend)\n\t\tflatOptions: {\n\t\t\turl: true,\n\t\t\tcontext: true\n\t\t}\n\t},\n\n\t// Creates a full fledged settings object into target\n\t// with both ajaxSettings and settings fields.\n\t// If target is omitted, writes into ajaxSettings.\n\tajaxSetup: function( target, settings ) {\n\t\treturn settings ?\n\n\t\t\t// Building a settings object\n\t\t\tajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :\n\n\t\t\t// Extending ajaxSettings\n\t\t\tajaxExtend( jQuery.ajaxSettings, target );\n\t},\n\n\tajaxPrefilter: addToPrefiltersOrTransports( prefilters ),\n\tajaxTransport: addToPrefiltersOrTransports( transports ),\n\n\t// Main method\n\tajax: function( url, options ) {\n\n\t\t// If url is an object, simulate pre-1.5 signature\n\t\tif ( typeof url === \"object\" ) {\n\t\t\toptions = url;\n\t\t\turl = undefined;\n\t\t}\n\n\t\t// Force options to be an object\n\t\toptions = options || {};\n\n\t\tvar transport,\n\n\t\t\t// URL without anti-cache param\n\t\t\tcacheURL,\n\n\t\t\t// Response headers\n\t\t\tresponseHeadersString,\n\t\t\tresponseHeaders,\n\n\t\t\t// timeout handle\n\t\t\ttimeoutTimer,\n\n\t\t\t// Url cleanup var\n\t\t\turlAnchor,\n\n\t\t\t// Request state (becomes false upon send and true upon completion)\n\t\t\tcompleted,\n\n\t\t\t// To know if global events are to be dispatched\n\t\t\tfireGlobals,\n\n\t\t\t// Loop variable\n\t\t\ti,\n\n\t\t\t// uncached part of the url\n\t\t\tuncached,\n\n\t\t\t// Create the final options object\n\t\t\ts = jQuery.ajaxSetup( {}, options ),\n\n\t\t\t// Callbacks context\n\t\t\tcallbackContext = s.context || s,\n\n\t\t\t// Context for global events is callbackContext if it is a DOM node or jQuery collection\n\t\t\tglobalEventContext = s.context &&\n\t\t\t\t( callbackContext.nodeType || callbackContext.jquery ) ?\n\t\t\t\tjQuery( callbackContext ) :\n\t\t\t\tjQuery.event,\n\n\t\t\t// Deferreds\n\t\t\tdeferred = jQuery.Deferred(),\n\t\t\tcompleteDeferred = jQuery.Callbacks( \"once memory\" ),\n\n\t\t\t// Status-dependent callbacks\n\t\t\tstatusCode = s.statusCode || {},\n\n\t\t\t// Headers (they are sent all at once)\n\t\t\trequestHeaders = {},\n\t\t\trequestHeadersNames = {},\n\n\t\t\t// Default abort message\n\t\t\tstrAbort = \"canceled\",\n\n\t\t\t// Fake xhr\n\t\t\tjqXHR = {\n\t\t\t\treadyState: 0,\n\n\t\t\t\t// Builds headers hashtable if needed\n\t\t\t\tgetResponseHeader: function( key ) {\n\t\t\t\t\tvar match;\n\t\t\t\t\tif ( completed ) {\n\t\t\t\t\t\tif ( !responseHeaders ) {\n\t\t\t\t\t\t\tresponseHeaders = {};\n\t\t\t\t\t\t\twhile ( ( match = rheaders.exec( responseHeadersString ) ) ) {\n\t\t\t\t\t\t\t\tresponseHeaders[ match[ 1 ].toLowerCase() + \" \" ] =\n\t\t\t\t\t\t\t\t\t( responseHeaders[ match[ 1 ].toLowerCase() + \" \" ] || [] )\n\t\t\t\t\t\t\t\t\t\t.concat( match[ 2 ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmatch = responseHeaders[ key.toLowerCase() + \" \" ];\n\t\t\t\t\t}\n\t\t\t\t\treturn match == null ? null : match.join( \", \" );\n\t\t\t\t},\n\n\t\t\t\t// Raw string\n\t\t\t\tgetAllResponseHeaders: function() {\n\t\t\t\t\treturn completed ? responseHeadersString : null;\n\t\t\t\t},\n\n\t\t\t\t// Caches the header\n\t\t\t\tsetRequestHeader: function( name, value ) {\n\t\t\t\t\tif ( completed == null ) {\n\t\t\t\t\t\tname = requestHeadersNames[ name.toLowerCase() ] =\n\t\t\t\t\t\t\trequestHeadersNames[ name.toLowerCase() ] || name;\n\t\t\t\t\t\trequestHeaders[ name ] = value;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Overrides response content-type header\n\t\t\t\toverrideMimeType: function( type ) {\n\t\t\t\t\tif ( completed == null ) {\n\t\t\t\t\t\ts.mimeType = type;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Status-dependent callbacks\n\t\t\t\tstatusCode: function( map ) {\n\t\t\t\t\tvar code;\n\t\t\t\t\tif ( map ) {\n\t\t\t\t\t\tif ( completed ) {\n\n\t\t\t\t\t\t\t// Execute the appropriate callbacks\n\t\t\t\t\t\t\tjqXHR.always( map[ jqXHR.status ] );\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t// Lazy-add the new callbacks in a way that preserves old ones\n\t\t\t\t\t\t\tfor ( code in map ) {\n\t\t\t\t\t\t\t\tstatusCode[ code ] = [ statusCode[ code ], map[ code ] ];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Cancel the request\n\t\t\t\tabort: function( statusText ) {\n\t\t\t\t\tvar finalText = statusText || strAbort;\n\t\t\t\t\tif ( transport ) {\n\t\t\t\t\t\ttransport.abort( finalText );\n\t\t\t\t\t}\n\t\t\t\t\tdone( 0, finalText );\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t};\n\n\t\t// Attach deferreds\n\t\tdeferred.promise( jqXHR );\n\n\t\t// Add protocol if not provided (prefilters might expect it)\n\t\t// Handle falsy url in the settings object (trac-10093: consistency with old signature)\n\t\t// We also use the url parameter if available\n\t\ts.url = ( ( url || s.url || location.href ) + \"\" )\n\t\t\t.replace( rprotocol, location.protocol + \"//\" );\n\n\t\t// Alias method option to type as per ticket trac-12004\n\t\ts.type = options.method || options.type || s.method || s.type;\n\n\t\t// Extract dataTypes list\n\t\ts.dataTypes = ( s.dataType || \"*\" ).toLowerCase().match( rnothtmlwhite ) || [ \"\" ];\n\n\t\t// A cross-domain request is in order when the origin doesn't match the current origin.\n\t\tif ( s.crossDomain == null ) {\n\t\t\turlAnchor = document.createElement( \"a\" );\n\n\t\t\t// Support: IE <=8 - 11, Edge 12 - 15\n\t\t\t// IE throws exception on accessing the href property if url is malformed,\n\t\t\t// e.g. http://example.com:80x/\n\t\t\ttry {\n\t\t\t\turlAnchor.href = s.url;\n\n\t\t\t\t// Support: IE <=8 - 11 only\n\t\t\t\t// Anchor's host property isn't correctly set when s.url is relative\n\t\t\t\turlAnchor.href = urlAnchor.href;\n\t\t\t\ts.crossDomain = originAnchor.protocol + \"//\" + originAnchor.host !==\n\t\t\t\t\turlAnchor.protocol + \"//\" + urlAnchor.host;\n\t\t\t} catch ( e ) {\n\n\t\t\t\t// If there is an error parsing the URL, assume it is crossDomain,\n\t\t\t\t// it can be rejected by the transport if it is invalid\n\t\t\t\ts.crossDomain = true;\n\t\t\t}\n\t\t}\n\n\t\t// Convert data if not already a string\n\t\tif ( s.data && s.processData && typeof s.data !== \"string\" ) {\n\t\t\ts.data = jQuery.param( s.data, s.traditional );\n\t\t}\n\n\t\t// Apply prefilters\n\t\tinspectPrefiltersOrTransports( prefilters, s, options, jqXHR );\n\n\t\t// If request was aborted inside a prefilter, stop there\n\t\tif ( completed ) {\n\t\t\treturn jqXHR;\n\t\t}\n\n\t\t// We can fire global events as of now if asked to\n\t\t// Don't fire events if jQuery.event is undefined in an AMD-usage scenario (trac-15118)\n\t\tfireGlobals = jQuery.event && s.global;\n\n\t\t// Watch for a new set of requests\n\t\tif ( fireGlobals && jQuery.active++ === 0 ) {\n\t\t\tjQuery.event.trigger( \"ajaxStart\" );\n\t\t}\n\n\t\t// Uppercase the type\n\t\ts.type = s.type.toUpperCase();\n\n\t\t// Determine if request has content\n\t\ts.hasContent = !rnoContent.test( s.type );\n\n\t\t// Save the URL in case we're toying with the If-Modified-Since\n\t\t// and/or If-None-Match header later on\n\t\t// Remove hash to simplify url manipulation\n\t\tcacheURL = s.url.replace( rhash, \"\" );\n\n\t\t// More options handling for requests with no content\n\t\tif ( !s.hasContent ) {\n\n\t\t\t// Remember the hash so we can put it back\n\t\t\tuncached = s.url.slice( cacheURL.length );\n\n\t\t\t// If data is available and should be processed, append data to url\n\t\t\tif ( s.data && ( s.processData || typeof s.data === \"string\" ) ) {\n\t\t\t\tcacheURL += ( rquery.test( cacheURL ) ? \"&\" : \"?\" ) + s.data;\n\n\t\t\t\t// trac-9682: remove data so that it's not used in an eventual retry\n\t\t\t\tdelete s.data;\n\t\t\t}\n\n\t\t\t// Add or update anti-cache param if needed\n\t\t\tif ( s.cache === false ) {\n\t\t\t\tcacheURL = cacheURL.replace( rantiCache, \"$1\" );\n\t\t\t\tuncached = ( rquery.test( cacheURL ) ? \"&\" : \"?\" ) + \"_=\" + ( nonce.guid++ ) +\n\t\t\t\t\tuncached;\n\t\t\t}\n\n\t\t\t// Put hash and anti-cache on the URL that will be requested (gh-1732)\n\t\t\ts.url = cacheURL + uncached;\n\n\t\t// Change '%20' to '+' if this is encoded form body content (gh-2658)\n\t\t} else if ( s.data && s.processData &&\n\t\t\t( s.contentType || \"\" ).indexOf( \"application/x-www-form-urlencoded\" ) === 0 ) {\n\t\t\ts.data = s.data.replace( r20, \"+\" );\n\t\t}\n\n\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\tif ( s.ifModified ) {\n\t\t\tif ( jQuery.lastModified[ cacheURL ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-Modified-Since\", jQuery.lastModified[ cacheURL ] );\n\t\t\t}\n\t\t\tif ( jQuery.etag[ cacheURL ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-None-Match\", jQuery.etag[ cacheURL ] );\n\t\t\t}\n\t\t}\n\n\t\t// Set the correct header, if data is being sent\n\t\tif ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {\n\t\t\tjqXHR.setRequestHeader( \"Content-Type\", s.contentType );\n\t\t}\n\n\t\t// Set the Accepts header for the server, depending on the dataType\n\t\tjqXHR.setRequestHeader(\n\t\t\t\"Accept\",\n\t\t\ts.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ?\n\t\t\t\ts.accepts[ s.dataTypes[ 0 ] ] +\n\t\t\t\t\t( s.dataTypes[ 0 ] !== \"*\" ? \", \" + allTypes + \"; q=0.01\" : \"\" ) :\n\t\t\t\ts.accepts[ \"*\" ]\n\t\t);\n\n\t\t// Check for headers option\n\t\tfor ( i in s.headers ) {\n\t\t\tjqXHR.setRequestHeader( i, s.headers[ i ] );\n\t\t}\n\n\t\t// Allow custom headers/mimetypes and early abort\n\t\tif ( s.beforeSend &&\n\t\t\t( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) {\n\n\t\t\t// Abort if not done already and return\n\t\t\treturn jqXHR.abort();\n\t\t}\n\n\t\t// Aborting is no longer a cancellation\n\t\tstrAbort = \"abort\";\n\n\t\t// Install callbacks on deferreds\n\t\tcompleteDeferred.add( s.complete );\n\t\tjqXHR.done( s.success );\n\t\tjqXHR.fail( s.error );\n\n\t\t// Get transport\n\t\ttransport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );\n\n\t\t// If no transport, we auto-abort\n\t\tif ( !transport ) {\n\t\t\tdone( -1, \"No Transport\" );\n\t\t} else {\n\t\t\tjqXHR.readyState = 1;\n\n\t\t\t// Send global event\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxSend\", [ jqXHR, s ] );\n\t\t\t}\n\n\t\t\t// If request was aborted inside ajaxSend, stop there\n\t\t\tif ( completed ) {\n\t\t\t\treturn jqXHR;\n\t\t\t}\n\n\t\t\t// Timeout\n\t\t\tif ( s.async && s.timeout > 0 ) {\n\t\t\t\ttimeoutTimer = window.setTimeout( function() {\n\t\t\t\t\tjqXHR.abort( \"timeout\" );\n\t\t\t\t}, s.timeout );\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tcompleted = false;\n\t\t\t\ttransport.send( requestHeaders, done );\n\t\t\t} catch ( e ) {\n\n\t\t\t\t// Rethrow post-completion exceptions\n\t\t\t\tif ( completed ) {\n\t\t\t\t\tthrow e;\n\t\t\t\t}\n\n\t\t\t\t// Propagate others as results\n\t\t\t\tdone( -1, e );\n\t\t\t}\n\t\t}\n\n\t\t// Callback for when everything is done\n\t\tfunction done( status, nativeStatusText, responses, headers ) {\n\t\t\tvar isSuccess, success, error, response, modified,\n\t\t\t\tstatusText = nativeStatusText;\n\n\t\t\t// Ignore repeat invocations\n\t\t\tif ( completed ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcompleted = true;\n\n\t\t\t// Clear timeout if it exists\n\t\t\tif ( timeoutTimer ) {\n\t\t\t\twindow.clearTimeout( timeoutTimer );\n\t\t\t}\n\n\t\t\t// Dereference transport for early garbage collection\n\t\t\t// (no matter how long the jqXHR object will be used)\n\t\t\ttransport = undefined;\n\n\t\t\t// Cache response headers\n\t\t\tresponseHeadersString = headers || \"\";\n\n\t\t\t// Set readyState\n\t\t\tjqXHR.readyState = status > 0 ? 4 : 0;\n\n\t\t\t// Determine if successful\n\t\t\tisSuccess = status >= 200 && status < 300 || status === 304;\n\n\t\t\t// Get response data\n\t\t\tif ( responses ) {\n\t\t\t\tresponse = ajaxHandleResponses( s, jqXHR, responses );\n\t\t\t}\n\n\t\t\t// Use a noop converter for missing script but not if jsonp\n\t\t\tif ( !isSuccess &&\n\t\t\t\tjQuery.inArray( \"script\", s.dataTypes ) > -1 &&\n\t\t\t\tjQuery.inArray( \"json\", s.dataTypes ) < 0 ) {\n\t\t\t\ts.converters[ \"text script\" ] = function() {};\n\t\t\t}\n\n\t\t\t// Convert no matter what (that way responseXXX fields are always set)\n\t\t\tresponse = ajaxConvert( s, response, jqXHR, isSuccess );\n\n\t\t\t// If successful, handle type chaining\n\t\t\tif ( isSuccess ) {\n\n\t\t\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\t\t\tif ( s.ifModified ) {\n\t\t\t\t\tmodified = jqXHR.getResponseHeader( \"Last-Modified\" );\n\t\t\t\t\tif ( modified ) {\n\t\t\t\t\t\tjQuery.lastModified[ cacheURL ] = modified;\n\t\t\t\t\t}\n\t\t\t\t\tmodified = jqXHR.getResponseHeader( \"etag\" );\n\t\t\t\t\tif ( modified ) {\n\t\t\t\t\t\tjQuery.etag[ cacheURL ] = modified;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// if no content\n\t\t\t\tif ( status === 204 || s.type === \"HEAD\" ) {\n\t\t\t\t\tstatusText = \"nocontent\";\n\n\t\t\t\t// if not modified\n\t\t\t\t} else if ( status === 304 ) {\n\t\t\t\t\tstatusText = \"notmodified\";\n\n\t\t\t\t// If we have data, let's convert it\n\t\t\t\t} else {\n\t\t\t\t\tstatusText = response.state;\n\t\t\t\t\tsuccess = response.data;\n\t\t\t\t\terror = response.error;\n\t\t\t\t\tisSuccess = !error;\n\t\t\t\t}\n\t\t\t} else {\n\n\t\t\t\t// Extract error from statusText and normalize for non-aborts\n\t\t\t\terror = statusText;\n\t\t\t\tif ( status || !statusText ) {\n\t\t\t\t\tstatusText = \"error\";\n\t\t\t\t\tif ( status < 0 ) {\n\t\t\t\t\t\tstatus = 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set data for the fake xhr object\n\t\t\tjqXHR.status = status;\n\t\t\tjqXHR.statusText = ( nativeStatusText || statusText ) + \"\";\n\n\t\t\t// Success/Error\n\t\t\tif ( isSuccess ) {\n\t\t\t\tdeferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );\n\t\t\t} else {\n\t\t\t\tdeferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );\n\t\t\t}\n\n\t\t\t// Status-dependent callbacks\n\t\t\tjqXHR.statusCode( statusCode );\n\t\t\tstatusCode = undefined;\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( isSuccess ? \"ajaxSuccess\" : \"ajaxError\",\n\t\t\t\t\t[ jqXHR, s, isSuccess ? success : error ] );\n\t\t\t}\n\n\t\t\t// Complete\n\t\t\tcompleteDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxComplete\", [ jqXHR, s ] );\n\n\t\t\t\t// Handle the global AJAX counter\n\t\t\t\tif ( !( --jQuery.active ) ) {\n\t\t\t\t\tjQuery.event.trigger( \"ajaxStop\" );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn jqXHR;\n\t},\n\n\tgetJSON: function( url, data, callback ) {\n\t\treturn jQuery.get( url, data, callback, \"json\" );\n\t},\n\n\tgetScript: function( url, callback ) {\n\t\treturn jQuery.get( url, undefined, callback, \"script\" );\n\t}\n} );\n\njQuery.each( [ \"get\", \"post\" ], function( _i, method ) {\n\tjQuery[ method ] = function( url, data, callback, type ) {\n\n\t\t// Shift arguments if data argument was omitted\n\t\tif ( isFunction( data ) ) {\n\t\t\ttype = type || callback;\n\t\t\tcallback = data;\n\t\t\tdata = undefined;\n\t\t}\n\n\t\t// The url can be an options object (which then must have .url)\n\t\treturn jQuery.ajax( jQuery.extend( {\n\t\t\turl: url,\n\t\t\ttype: method,\n\t\t\tdataType: type,\n\t\t\tdata: data,\n\t\t\tsuccess: callback\n\t\t}, jQuery.isPlainObject( url ) && url ) );\n\t};\n} );\n\njQuery.ajaxPrefilter( function( s ) {\n\tvar i;\n\tfor ( i in s.headers ) {\n\t\tif ( i.toLowerCase() === \"content-type\" ) {\n\t\t\ts.contentType = s.headers[ i ] || \"\";\n\t\t}\n\t}\n} );\n\n\njQuery._evalUrl = function( url, options, doc ) {\n\treturn jQuery.ajax( {\n\t\turl: url,\n\n\t\t// Make this explicit, since user can override this through ajaxSetup (trac-11264)\n\t\ttype: \"GET\",\n\t\tdataType: \"script\",\n\t\tcache: true,\n\t\tasync: false,\n\t\tglobal: false,\n\n\t\t// Only evaluate the response if it is successful (gh-4126)\n\t\t// dataFilter is not invoked for failure responses, so using it instead\n\t\t// of the default converter is kludgy but it works.\n\t\tconverters: {\n\t\t\t\"text script\": function() {}\n\t\t},\n\t\tdataFilter: function( response ) {\n\t\t\tjQuery.globalEval( response, options, doc );\n\t\t}\n\t} );\n};\n\n\njQuery.fn.extend( {\n\twrapAll: function( html ) {\n\t\tvar wrap;\n\n\t\tif ( this[ 0 ] ) {\n\t\t\tif ( isFunction( html ) ) {\n\t\t\t\thtml = html.call( this[ 0 ] );\n\t\t\t}\n\n\t\t\t// The elements to wrap the target around\n\t\t\twrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );\n\n\t\t\tif ( this[ 0 ].parentNode ) {\n\t\t\t\twrap.insertBefore( this[ 0 ] );\n\t\t\t}\n\n\t\t\twrap.map( function() {\n\t\t\t\tvar elem = this;\n\n\t\t\t\twhile ( elem.firstElementChild ) {\n\t\t\t\t\telem = elem.firstElementChild;\n\t\t\t\t}\n\n\t\t\t\treturn elem;\n\t\t\t} ).append( this );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\twrapInner: function( html ) {\n\t\tif ( isFunction( html ) ) {\n\t\t\treturn this.each( function( i ) {\n\t\t\t\tjQuery( this ).wrapInner( html.call( this, i ) );\n\t\t\t} );\n\t\t}\n\n\t\treturn this.each( function() {\n\t\t\tvar self = jQuery( this ),\n\t\t\t\tcontents = self.contents();\n\n\t\t\tif ( contents.length ) {\n\t\t\t\tcontents.wrapAll( html );\n\n\t\t\t} else {\n\t\t\t\tself.append( html );\n\t\t\t}\n\t\t} );\n\t},\n\n\twrap: function( html ) {\n\t\tvar htmlIsFunction = isFunction( html );\n\n\t\treturn this.each( function( i ) {\n\t\t\tjQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html );\n\t\t} );\n\t},\n\n\tunwrap: function( selector ) {\n\t\tthis.parent( selector ).not( \"body\" ).each( function() {\n\t\t\tjQuery( this ).replaceWith( this.childNodes );\n\t\t} );\n\t\treturn this;\n\t}\n} );\n\n\njQuery.expr.pseudos.hidden = function( elem ) {\n\treturn !jQuery.expr.pseudos.visible( elem );\n};\njQuery.expr.pseudos.visible = function( elem ) {\n\treturn !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );\n};\n\n\n\n\njQuery.ajaxSettings.xhr = function() {\n\ttry {\n\t\treturn new window.XMLHttpRequest();\n\t} catch ( e ) {}\n};\n\nvar xhrSuccessStatus = {\n\n\t\t// File protocol always yields status code 0, assume 200\n\t\t0: 200,\n\n\t\t// Support: IE <=9 only\n\t\t// trac-1450: sometimes IE returns 1223 when it should be 204\n\t\t1223: 204\n\t},\n\txhrSupported = jQuery.ajaxSettings.xhr();\n\nsupport.cors = !!xhrSupported && ( \"withCredentials\" in xhrSupported );\nsupport.ajax = xhrSupported = !!xhrSupported;\n\njQuery.ajaxTransport( function( options ) {\n\tvar callback, errorCallback;\n\n\t// Cross domain only allowed if supported through XMLHttpRequest\n\tif ( support.cors || xhrSupported && !options.crossDomain ) {\n\t\treturn {\n\t\t\tsend: function( headers, complete ) {\n\t\t\t\tvar i,\n\t\t\t\t\txhr = options.xhr();\n\n\t\t\t\txhr.open(\n\t\t\t\t\toptions.type,\n\t\t\t\t\toptions.url,\n\t\t\t\t\toptions.async,\n\t\t\t\t\toptions.username,\n\t\t\t\t\toptions.password\n\t\t\t\t);\n\n\t\t\t\t// Apply custom fields if provided\n\t\t\t\tif ( options.xhrFields ) {\n\t\t\t\t\tfor ( i in options.xhrFields ) {\n\t\t\t\t\t\txhr[ i ] = options.xhrFields[ i ];\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Override mime type if needed\n\t\t\t\tif ( options.mimeType && xhr.overrideMimeType ) {\n\t\t\t\t\txhr.overrideMimeType( options.mimeType );\n\t\t\t\t}\n\n\t\t\t\t// X-Requested-With header\n\t\t\t\t// For cross-domain requests, seeing as conditions for a preflight are\n\t\t\t\t// akin to a jigsaw puzzle, we simply never set it to be sure.\n\t\t\t\t// (it can always be set on a per-request basis or even using ajaxSetup)\n\t\t\t\t// For same-domain requests, won't change header if already provided.\n\t\t\t\tif ( !options.crossDomain && !headers[ \"X-Requested-With\" ] ) {\n\t\t\t\t\theaders[ \"X-Requested-With\" ] = \"XMLHttpRequest\";\n\t\t\t\t}\n\n\t\t\t\t// Set headers\n\t\t\t\tfor ( i in headers ) {\n\t\t\t\t\txhr.setRequestHeader( i, headers[ i ] );\n\t\t\t\t}\n\n\t\t\t\t// Callback\n\t\t\t\tcallback = function( type ) {\n\t\t\t\t\treturn function() {\n\t\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\t\tcallback = errorCallback = xhr.onload =\n\t\t\t\t\t\t\t\txhr.onerror = xhr.onabort = xhr.ontimeout =\n\t\t\t\t\t\t\t\t\txhr.onreadystatechange = null;\n\n\t\t\t\t\t\t\tif ( type === \"abort\" ) {\n\t\t\t\t\t\t\t\txhr.abort();\n\t\t\t\t\t\t\t} else if ( type === \"error\" ) {\n\n\t\t\t\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t\t\t\t// On a manual native abort, IE9 throws\n\t\t\t\t\t\t\t\t// errors on any property access that is not readyState\n\t\t\t\t\t\t\t\tif ( typeof xhr.status !== \"number\" ) {\n\t\t\t\t\t\t\t\t\tcomplete( 0, \"error\" );\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tcomplete(\n\n\t\t\t\t\t\t\t\t\t\t// File: protocol always yields status 0; see trac-8605, trac-14207\n\t\t\t\t\t\t\t\t\t\txhr.status,\n\t\t\t\t\t\t\t\t\t\txhr.statusText\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcomplete(\n\t\t\t\t\t\t\t\t\txhrSuccessStatus[ xhr.status ] || xhr.status,\n\t\t\t\t\t\t\t\t\txhr.statusText,\n\n\t\t\t\t\t\t\t\t\t// Support: IE <=9 only\n\t\t\t\t\t\t\t\t\t// IE9 has no XHR2 but throws on binary (trac-11426)\n\t\t\t\t\t\t\t\t\t// For XHR2 non-text, let the caller handle it (gh-2498)\n\t\t\t\t\t\t\t\t\t( xhr.responseType || \"text\" ) !== \"text\"  ||\n\t\t\t\t\t\t\t\t\ttypeof xhr.responseText !== \"string\" ?\n\t\t\t\t\t\t\t\t\t\t{ binary: xhr.response } :\n\t\t\t\t\t\t\t\t\t\t{ text: xhr.responseText },\n\t\t\t\t\t\t\t\t\txhr.getAllResponseHeaders()\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t};\n\n\t\t\t\t// Listen to events\n\t\t\t\txhr.onload = callback();\n\t\t\t\terrorCallback = xhr.onerror = xhr.ontimeout = callback( \"error\" );\n\n\t\t\t\t// Support: IE 9 only\n\t\t\t\t// Use onreadystatechange to replace onabort\n\t\t\t\t// to handle uncaught aborts\n\t\t\t\tif ( xhr.onabort !== undefined ) {\n\t\t\t\t\txhr.onabort = errorCallback;\n\t\t\t\t} else {\n\t\t\t\t\txhr.onreadystatechange = function() {\n\n\t\t\t\t\t\t// Check readyState before timeout as it changes\n\t\t\t\t\t\tif ( xhr.readyState === 4 ) {\n\n\t\t\t\t\t\t\t// Allow onerror to be called first,\n\t\t\t\t\t\t\t// but that will not handle a native abort\n\t\t\t\t\t\t\t// Also, save errorCallback to a variable\n\t\t\t\t\t\t\t// as xhr.onerror cannot be accessed\n\t\t\t\t\t\t\twindow.setTimeout( function() {\n\t\t\t\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\t\t\t\terrorCallback();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\t// Create the abort callback\n\t\t\t\tcallback = callback( \"abort\" );\n\n\t\t\t\ttry {\n\n\t\t\t\t\t// Do send the request (this may raise an exception)\n\t\t\t\t\txhr.send( options.hasContent && options.data || null );\n\t\t\t\t} catch ( e ) {\n\n\t\t\t\t\t// trac-14683: Only rethrow if this hasn't been notified as an error yet\n\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\tthrow e;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tabort: function() {\n\t\t\t\tif ( callback ) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n} );\n\n\n\n\n// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432)\njQuery.ajaxPrefilter( function( s ) {\n\tif ( s.crossDomain ) {\n\t\ts.contents.script = false;\n\t}\n} );\n\n// Install script dataType\njQuery.ajaxSetup( {\n\taccepts: {\n\t\tscript: \"text/javascript, application/javascript, \" +\n\t\t\t\"application/ecmascript, application/x-ecmascript\"\n\t},\n\tcontents: {\n\t\tscript: /\\b(?:java|ecma)script\\b/\n\t},\n\tconverters: {\n\t\t\"text script\": function( text ) {\n\t\t\tjQuery.globalEval( text );\n\t\t\treturn text;\n\t\t}\n\t}\n} );\n\n// Handle cache's special case and crossDomain\njQuery.ajaxPrefilter( \"script\", function( s ) {\n\tif ( s.cache === undefined ) {\n\t\ts.cache = false;\n\t}\n\tif ( s.crossDomain ) {\n\t\ts.type = \"GET\";\n\t}\n} );\n\n// Bind script tag hack transport\njQuery.ajaxTransport( \"script\", function( s ) {\n\n\t// This transport only deals with cross domain or forced-by-attrs requests\n\tif ( s.crossDomain || s.scriptAttrs ) {\n\t\tvar script, callback;\n\t\treturn {\n\t\t\tsend: function( _, complete ) {\n\t\t\t\tscript = jQuery( \"<script>\" )\n\t\t\t\t\t.attr( s.scriptAttrs || {} )\n\t\t\t\t\t.prop( { charset: s.scriptCharset, src: s.url } )\n\t\t\t\t\t.on( \"load error\", callback = function( evt ) {\n\t\t\t\t\t\tscript.remove();\n\t\t\t\t\t\tcallback = null;\n\t\t\t\t\t\tif ( evt ) {\n\t\t\t\t\t\t\tcomplete( evt.type === \"error\" ? 404 : 200, evt.type );\n\t\t\t\t\t\t}\n\t\t\t\t\t} );\n\n\t\t\t\t// Use native DOM manipulation to avoid our domManip AJAX trickery\n\t\t\t\tdocument.head.appendChild( script[ 0 ] );\n\t\t\t},\n\t\t\tabort: function() {\n\t\t\t\tif ( callback ) {\n\t\t\t\t\tcallback();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n} );\n\n\n\n\nvar oldCallbacks = [],\n\trjsonp = /(=)\\?(?=&|$)|\\?\\?/;\n\n// Default jsonp settings\njQuery.ajaxSetup( {\n\tjsonp: \"callback\",\n\tjsonpCallback: function() {\n\t\tvar callback = oldCallbacks.pop() || ( jQuery.expando + \"_\" + ( nonce.guid++ ) );\n\t\tthis[ callback ] = true;\n\t\treturn callback;\n\t}\n} );\n\n// Detect, normalize options and install callbacks for jsonp requests\njQuery.ajaxPrefilter( \"json jsonp\", function( s, originalSettings, jqXHR ) {\n\n\tvar callbackName, overwritten, responseContainer,\n\t\tjsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?\n\t\t\t\"url\" :\n\t\t\ttypeof s.data === \"string\" &&\n\t\t\t\t( s.contentType || \"\" )\n\t\t\t\t\t.indexOf( \"application/x-www-form-urlencoded\" ) === 0 &&\n\t\t\t\trjsonp.test( s.data ) && \"data\"\n\t\t);\n\n\t// Handle iff the expected data type is \"jsonp\" or we have a parameter to set\n\tif ( jsonProp || s.dataTypes[ 0 ] === \"jsonp\" ) {\n\n\t\t// Get callback name, remembering preexisting value associated with it\n\t\tcallbackName = s.jsonpCallback = isFunction( s.jsonpCallback ) ?\n\t\t\ts.jsonpCallback() :\n\t\t\ts.jsonpCallback;\n\n\t\t// Insert callback into url or form data\n\t\tif ( jsonProp ) {\n\t\t\ts[ jsonProp ] = s[ jsonProp ].replace( rjsonp, \"$1\" + callbackName );\n\t\t} else if ( s.jsonp !== false ) {\n\t\t\ts.url += ( rquery.test( s.url ) ? \"&\" : \"?\" ) + s.jsonp + \"=\" + callbackName;\n\t\t}\n\n\t\t// Use data converter to retrieve json after script execution\n\t\ts.converters[ \"script json\" ] = function() {\n\t\t\tif ( !responseContainer ) {\n\t\t\t\tjQuery.error( callbackName + \" was not called\" );\n\t\t\t}\n\t\t\treturn responseContainer[ 0 ];\n\t\t};\n\n\t\t// Force json dataType\n\t\ts.dataTypes[ 0 ] = \"json\";\n\n\t\t// Install callback\n\t\toverwritten = window[ callbackName ];\n\t\twindow[ callbackName ] = function() {\n\t\t\tresponseContainer = arguments;\n\t\t};\n\n\t\t// Clean-up function (fires after converters)\n\t\tjqXHR.always( function() {\n\n\t\t\t// If previous value didn't exist - remove it\n\t\t\tif ( overwritten === undefined ) {\n\t\t\t\tjQuery( window ).removeProp( callbackName );\n\n\t\t\t// Otherwise restore preexisting value\n\t\t\t} else {\n\t\t\t\twindow[ callbackName ] = overwritten;\n\t\t\t}\n\n\t\t\t// Save back as free\n\t\t\tif ( s[ callbackName ] ) {\n\n\t\t\t\t// Make sure that re-using the options doesn't screw things around\n\t\t\t\ts.jsonpCallback = originalSettings.jsonpCallback;\n\n\t\t\t\t// Save the callback name for future use\n\t\t\t\toldCallbacks.push( callbackName );\n\t\t\t}\n\n\t\t\t// Call if it was a function and we have a response\n\t\t\tif ( responseContainer && isFunction( overwritten ) ) {\n\t\t\t\toverwritten( responseContainer[ 0 ] );\n\t\t\t}\n\n\t\t\tresponseContainer = overwritten = undefined;\n\t\t} );\n\n\t\t// Delegate to script\n\t\treturn \"script\";\n\t}\n} );\n\n\n\n\n// Support: Safari 8 only\n// In Safari 8 documents created via document.implementation.createHTMLDocument\n// collapse sibling forms: the second one becomes a child of the first one.\n// Because of that, this security measure has to be disabled in Safari 8.\n// https://bugs.webkit.org/show_bug.cgi?id=137337\nsupport.createHTMLDocument = ( function() {\n\tvar body = document.implementation.createHTMLDocument( \"\" ).body;\n\tbody.innerHTML = \"<form></form><form></form>\";\n\treturn body.childNodes.length === 2;\n} )();\n\n\n// Argument \"data\" should be string of html\n// context (optional): If specified, the fragment will be created in this context,\n// defaults to document\n// keepScripts (optional): If true, will include scripts passed in the html string\njQuery.parseHTML = function( data, context, keepScripts ) {\n\tif ( typeof data !== \"string\" ) {\n\t\treturn [];\n\t}\n\tif ( typeof context === \"boolean\" ) {\n\t\tkeepScripts = context;\n\t\tcontext = false;\n\t}\n\n\tvar base, parsed, scripts;\n\n\tif ( !context ) {\n\n\t\t// Stop scripts or inline event handlers from being executed immediately\n\t\t// by using document.implementation\n\t\tif ( support.createHTMLDocument ) {\n\t\t\tcontext = document.implementation.createHTMLDocument( \"\" );\n\n\t\t\t// Set the base href for the created document\n\t\t\t// so any parsed elements with URLs\n\t\t\t// are based on the document's URL (gh-2965)\n\t\t\tbase = context.createElement( \"base\" );\n\t\t\tbase.href = document.location.href;\n\t\t\tcontext.head.appendChild( base );\n\t\t} else {\n\t\t\tcontext = document;\n\t\t}\n\t}\n\n\tparsed = rsingleTag.exec( data );\n\tscripts = !keepScripts && [];\n\n\t// Single tag\n\tif ( parsed ) {\n\t\treturn [ context.createElement( parsed[ 1 ] ) ];\n\t}\n\n\tparsed = buildFragment( [ data ], context, scripts );\n\n\tif ( scripts && scripts.length ) {\n\t\tjQuery( scripts ).remove();\n\t}\n\n\treturn jQuery.merge( [], parsed.childNodes );\n};\n\n\n/**\n * Load a url into a page\n */\njQuery.fn.load = function( url, params, callback ) {\n\tvar selector, type, response,\n\t\tself = this,\n\t\toff = url.indexOf( \" \" );\n\n\tif ( off > -1 ) {\n\t\tselector = stripAndCollapse( url.slice( off ) );\n\t\turl = url.slice( 0, off );\n\t}\n\n\t// If it's a function\n\tif ( isFunction( params ) ) {\n\n\t\t// We assume that it's the callback\n\t\tcallback = params;\n\t\tparams = undefined;\n\n\t// Otherwise, build a param string\n\t} else if ( params && typeof params === \"object\" ) {\n\t\ttype = \"POST\";\n\t}\n\n\t// If we have elements to modify, make the request\n\tif ( self.length > 0 ) {\n\t\tjQuery.ajax( {\n\t\t\turl: url,\n\n\t\t\t// If \"type\" variable is undefined, then \"GET\" method will be used.\n\t\t\t// Make value of this field explicit since\n\t\t\t// user can override it through ajaxSetup method\n\t\t\ttype: type || \"GET\",\n\t\t\tdataType: \"html\",\n\t\t\tdata: params\n\t\t} ).done( function( responseText ) {\n\n\t\t\t// Save response for use in complete callback\n\t\t\tresponse = arguments;\n\n\t\t\tself.html( selector ?\n\n\t\t\t\t// If a selector was specified, locate the right elements in a dummy div\n\t\t\t\t// Exclude scripts to avoid IE 'Permission Denied' errors\n\t\t\t\tjQuery( \"<div>\" ).append( jQuery.parseHTML( responseText ) ).find( selector ) :\n\n\t\t\t\t// Otherwise use the full result\n\t\t\t\tresponseText );\n\n\t\t// If the request succeeds, this function gets \"data\", \"status\", \"jqXHR\"\n\t\t// but they are ignored because response was set above.\n\t\t// If it fails, this function gets \"jqXHR\", \"status\", \"error\"\n\t\t} ).always( callback && function( jqXHR, status ) {\n\t\t\tself.each( function() {\n\t\t\t\tcallback.apply( this, response || [ jqXHR.responseText, status, jqXHR ] );\n\t\t\t} );\n\t\t} );\n\t}\n\n\treturn this;\n};\n\n\n\n\njQuery.expr.pseudos.animated = function( elem ) {\n\treturn jQuery.grep( jQuery.timers, function( fn ) {\n\t\treturn elem === fn.elem;\n\t} ).length;\n};\n\n\n\n\njQuery.offset = {\n\tsetOffset: function( elem, options, i ) {\n\t\tvar curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,\n\t\t\tposition = jQuery.css( elem, \"position\" ),\n\t\t\tcurElem = jQuery( elem ),\n\t\t\tprops = {};\n\n\t\t// Set position first, in-case top/left are set even on static elem\n\t\tif ( position === \"static\" ) {\n\t\t\telem.style.position = \"relative\";\n\t\t}\n\n\t\tcurOffset = curElem.offset();\n\t\tcurCSSTop = jQuery.css( elem, \"top\" );\n\t\tcurCSSLeft = jQuery.css( elem, \"left\" );\n\t\tcalculatePosition = ( position === \"absolute\" || position === \"fixed\" ) &&\n\t\t\t( curCSSTop + curCSSLeft ).indexOf( \"auto\" ) > -1;\n\n\t\t// Need to be able to calculate position if either\n\t\t// top or left is auto and position is either absolute or fixed\n\t\tif ( calculatePosition ) {\n\t\t\tcurPosition = curElem.position();\n\t\t\tcurTop = curPosition.top;\n\t\t\tcurLeft = curPosition.left;\n\n\t\t} else {\n\t\t\tcurTop = parseFloat( curCSSTop ) || 0;\n\t\t\tcurLeft = parseFloat( curCSSLeft ) || 0;\n\t\t}\n\n\t\tif ( isFunction( options ) ) {\n\n\t\t\t// Use jQuery.extend here to allow modification of coordinates argument (gh-1848)\n\t\t\toptions = options.call( elem, i, jQuery.extend( {}, curOffset ) );\n\t\t}\n\n\t\tif ( options.top != null ) {\n\t\t\tprops.top = ( options.top - curOffset.top ) + curTop;\n\t\t}\n\t\tif ( options.left != null ) {\n\t\t\tprops.left = ( options.left - curOffset.left ) + curLeft;\n\t\t}\n\n\t\tif ( \"using\" in options ) {\n\t\t\toptions.using.call( elem, props );\n\n\t\t} else {\n\t\t\tcurElem.css( props );\n\t\t}\n\t}\n};\n\njQuery.fn.extend( {\n\n\t// offset() relates an element's border box to the document origin\n\toffset: function( options ) {\n\n\t\t// Preserve chaining for setter\n\t\tif ( arguments.length ) {\n\t\t\treturn options === undefined ?\n\t\t\t\tthis :\n\t\t\t\tthis.each( function( i ) {\n\t\t\t\t\tjQuery.offset.setOffset( this, options, i );\n\t\t\t\t} );\n\t\t}\n\n\t\tvar rect, win,\n\t\t\telem = this[ 0 ];\n\n\t\tif ( !elem ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Return zeros for disconnected and hidden (display: none) elements (gh-2310)\n\t\t// Support: IE <=11 only\n\t\t// Running getBoundingClientRect on a\n\t\t// disconnected node in IE throws an error\n\t\tif ( !elem.getClientRects().length ) {\n\t\t\treturn { top: 0, left: 0 };\n\t\t}\n\n\t\t// Get document-relative position by adding viewport scroll to viewport-relative gBCR\n\t\trect = elem.getBoundingClientRect();\n\t\twin = elem.ownerDocument.defaultView;\n\t\treturn {\n\t\t\ttop: rect.top + win.pageYOffset,\n\t\t\tleft: rect.left + win.pageXOffset\n\t\t};\n\t},\n\n\t// position() relates an element's margin box to its offset parent's padding box\n\t// This corresponds to the behavior of CSS absolute positioning\n\tposition: function() {\n\t\tif ( !this[ 0 ] ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar offsetParent, offset, doc,\n\t\t\telem = this[ 0 ],\n\t\t\tparentOffset = { top: 0, left: 0 };\n\n\t\t// position:fixed elements are offset from the viewport, which itself always has zero offset\n\t\tif ( jQuery.css( elem, \"position\" ) === \"fixed\" ) {\n\n\t\t\t// Assume position:fixed implies availability of getBoundingClientRect\n\t\t\toffset = elem.getBoundingClientRect();\n\n\t\t} else {\n\t\t\toffset = this.offset();\n\n\t\t\t// Account for the *real* offset parent, which can be the document or its root element\n\t\t\t// when a statically positioned element is identified\n\t\t\tdoc = elem.ownerDocument;\n\t\t\toffsetParent = elem.offsetParent || doc.documentElement;\n\t\t\twhile ( offsetParent &&\n\t\t\t\t( offsetParent === doc.body || offsetParent === doc.documentElement ) &&\n\t\t\t\tjQuery.css( offsetParent, \"position\" ) === \"static\" ) {\n\n\t\t\t\toffsetParent = offsetParent.parentNode;\n\t\t\t}\n\t\t\tif ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 ) {\n\n\t\t\t\t// Incorporate borders into its offset, since they are outside its content origin\n\t\t\t\tparentOffset = jQuery( offsetParent ).offset();\n\t\t\t\tparentOffset.top += jQuery.css( offsetParent, \"borderTopWidth\", true );\n\t\t\t\tparentOffset.left += jQuery.css( offsetParent, \"borderLeftWidth\", true );\n\t\t\t}\n\t\t}\n\n\t\t// Subtract parent offsets and element margins\n\t\treturn {\n\t\t\ttop: offset.top - parentOffset.top - jQuery.css( elem, \"marginTop\", true ),\n\t\t\tleft: offset.left - parentOffset.left - jQuery.css( elem, \"marginLeft\", true )\n\t\t};\n\t},\n\n\t// This method will return documentElement in the following cases:\n\t// 1) For the element inside the iframe without offsetParent, this method will return\n\t//    documentElement of the parent window\n\t// 2) For the hidden or detached element\n\t// 3) For body or html element, i.e. in case of the html node - it will return itself\n\t//\n\t// but those exceptions were never presented as a real life use-cases\n\t// and might be considered as more preferable results.\n\t//\n\t// This logic, however, is not guaranteed and can change at any point in the future\n\toffsetParent: function() {\n\t\treturn this.map( function() {\n\t\t\tvar offsetParent = this.offsetParent;\n\n\t\t\twhile ( offsetParent && jQuery.css( offsetParent, \"position\" ) === \"static\" ) {\n\t\t\t\toffsetParent = offsetParent.offsetParent;\n\t\t\t}\n\n\t\t\treturn offsetParent || documentElement;\n\t\t} );\n\t}\n} );\n\n// Create scrollLeft and scrollTop methods\njQuery.each( { scrollLeft: \"pageXOffset\", scrollTop: \"pageYOffset\" }, function( method, prop ) {\n\tvar top = \"pageYOffset\" === prop;\n\n\tjQuery.fn[ method ] = function( val ) {\n\t\treturn access( this, function( elem, method, val ) {\n\n\t\t\t// Coalesce documents and windows\n\t\t\tvar win;\n\t\t\tif ( isWindow( elem ) ) {\n\t\t\t\twin = elem;\n\t\t\t} else if ( elem.nodeType === 9 ) {\n\t\t\t\twin = elem.defaultView;\n\t\t\t}\n\n\t\t\tif ( val === undefined ) {\n\t\t\t\treturn win ? win[ prop ] : elem[ method ];\n\t\t\t}\n\n\t\t\tif ( win ) {\n\t\t\t\twin.scrollTo(\n\t\t\t\t\t!top ? val : win.pageXOffset,\n\t\t\t\t\ttop ? val : win.pageYOffset\n\t\t\t\t);\n\n\t\t\t} else {\n\t\t\t\telem[ method ] = val;\n\t\t\t}\n\t\t}, method, val, arguments.length );\n\t};\n} );\n\n// Support: Safari <=7 - 9.1, Chrome <=37 - 49\n// Add the top/left cssHooks using jQuery.fn.position\n// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084\n// Blink bug: https://bugs.chromium.org/p/chromium/issues/detail?id=589347\n// getComputedStyle returns percent when specified for top/left/bottom/right;\n// rather than make the css module depend on the offset module, just check for it here\njQuery.each( [ \"top\", \"left\" ], function( _i, prop ) {\n\tjQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,\n\t\tfunction( elem, computed ) {\n\t\t\tif ( computed ) {\n\t\t\t\tcomputed = curCSS( elem, prop );\n\n\t\t\t\t// If curCSS returns percentage, fallback to offset\n\t\t\t\treturn rnumnonpx.test( computed ) ?\n\t\t\t\t\tjQuery( elem ).position()[ prop ] + \"px\" :\n\t\t\t\t\tcomputed;\n\t\t\t}\n\t\t}\n\t);\n} );\n\n\n// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods\njQuery.each( { Height: \"height\", Width: \"width\" }, function( name, type ) {\n\tjQuery.each( {\n\t\tpadding: \"inner\" + name,\n\t\tcontent: type,\n\t\t\"\": \"outer\" + name\n\t}, function( defaultExtra, funcName ) {\n\n\t\t// Margin is only for outerHeight, outerWidth\n\t\tjQuery.fn[ funcName ] = function( margin, value ) {\n\t\t\tvar chainable = arguments.length && ( defaultExtra || typeof margin !== \"boolean\" ),\n\t\t\t\textra = defaultExtra || ( margin === true || value === true ? \"margin\" : \"border\" );\n\n\t\t\treturn access( this, function( elem, type, value ) {\n\t\t\t\tvar doc;\n\n\t\t\t\tif ( isWindow( elem ) ) {\n\n\t\t\t\t\t// $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)\n\t\t\t\t\treturn funcName.indexOf( \"outer\" ) === 0 ?\n\t\t\t\t\t\telem[ \"inner\" + name ] :\n\t\t\t\t\t\telem.document.documentElement[ \"client\" + name ];\n\t\t\t\t}\n\n\t\t\t\t// Get document width or height\n\t\t\t\tif ( elem.nodeType === 9 ) {\n\t\t\t\t\tdoc = elem.documentElement;\n\n\t\t\t\t\t// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],\n\t\t\t\t\t// whichever is greatest\n\t\t\t\t\treturn Math.max(\n\t\t\t\t\t\telem.body[ \"scroll\" + name ], doc[ \"scroll\" + name ],\n\t\t\t\t\t\telem.body[ \"offset\" + name ], doc[ \"offset\" + name ],\n\t\t\t\t\t\tdoc[ \"client\" + name ]\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn value === undefined ?\n\n\t\t\t\t\t// Get width or height on the element, requesting but not forcing parseFloat\n\t\t\t\t\tjQuery.css( elem, type, extra ) :\n\n\t\t\t\t\t// Set width or height on the element\n\t\t\t\t\tjQuery.style( elem, type, value, extra );\n\t\t\t}, type, chainable ? margin : undefined, chainable );\n\t\t};\n\t} );\n} );\n\n\njQuery.each( [\n\t\"ajaxStart\",\n\t\"ajaxStop\",\n\t\"ajaxComplete\",\n\t\"ajaxError\",\n\t\"ajaxSuccess\",\n\t\"ajaxSend\"\n], function( _i, type ) {\n\tjQuery.fn[ type ] = function( fn ) {\n\t\treturn this.on( type, fn );\n\t};\n} );\n\n\n\n\njQuery.fn.extend( {\n\n\tbind: function( types, data, fn ) {\n\t\treturn this.on( types, null, data, fn );\n\t},\n\tunbind: function( types, fn ) {\n\t\treturn this.off( types, null, fn );\n\t},\n\n\tdelegate: function( selector, types, data, fn ) {\n\t\treturn this.on( types, selector, data, fn );\n\t},\n\tundelegate: function( selector, types, fn ) {\n\n\t\t// ( namespace ) or ( selector, types [, fn] )\n\t\treturn arguments.length === 1 ?\n\t\t\tthis.off( selector, \"**\" ) :\n\t\t\tthis.off( types, selector || \"**\", fn );\n\t},\n\n\thover: function( fnOver, fnOut ) {\n\t\treturn this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );\n\t}\n} );\n\njQuery.each(\n\t( \"blur focus focusin focusout resize scroll click dblclick \" +\n\t\"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave \" +\n\t\"change select submit keydown keypress keyup contextmenu\" ).split( \" \" ),\n\tfunction( _i, name ) {\n\n\t\t// Handle event binding\n\t\tjQuery.fn[ name ] = function( data, fn ) {\n\t\t\treturn arguments.length > 0 ?\n\t\t\t\tthis.on( name, null, data, fn ) :\n\t\t\t\tthis.trigger( name );\n\t\t};\n\t}\n);\n\n\n\n\n// Support: Android <=4.0 only\n// Make sure we trim BOM and NBSP\n// Require that the \"whitespace run\" starts from a non-whitespace\n// to avoid O(N^2) behavior when the engine would try matching \"\\s+$\" at each space position.\nvar rtrim = /^[\\s\\uFEFF\\xA0]+|([^\\s\\uFEFF\\xA0])[\\s\\uFEFF\\xA0]+$/g;\n\n// Bind a function to a context, optionally partially applying any\n// arguments.\n// jQuery.proxy is deprecated to promote standards (specifically Function#bind)\n// However, it is not slated for removal any time soon\njQuery.proxy = function( fn, context ) {\n\tvar tmp, args, proxy;\n\n\tif ( typeof context === \"string\" ) {\n\t\ttmp = fn[ context ];\n\t\tcontext = fn;\n\t\tfn = tmp;\n\t}\n\n\t// Quick check to determine if target is callable, in the spec\n\t// this throws a TypeError, but we will just return undefined.\n\tif ( !isFunction( fn ) ) {\n\t\treturn undefined;\n\t}\n\n\t// Simulated bind\n\targs = slice.call( arguments, 2 );\n\tproxy = function() {\n\t\treturn fn.apply( context || this, args.concat( slice.call( arguments ) ) );\n\t};\n\n\t// Set the guid of unique handler to the same of original handler, so it can be removed\n\tproxy.guid = fn.guid = fn.guid || jQuery.guid++;\n\n\treturn proxy;\n};\n\njQuery.holdReady = function( hold ) {\n\tif ( hold ) {\n\t\tjQuery.readyWait++;\n\t} else {\n\t\tjQuery.ready( true );\n\t}\n};\njQuery.isArray = Array.isArray;\njQuery.parseJSON = JSON.parse;\njQuery.nodeName = nodeName;\njQuery.isFunction = isFunction;\njQuery.isWindow = isWindow;\njQuery.camelCase = camelCase;\njQuery.type = toType;\n\njQuery.now = Date.now;\n\njQuery.isNumeric = function( obj ) {\n\n\t// As of jQuery 3.0, isNumeric is limited to\n\t// strings and numbers (primitives or objects)\n\t// that can be coerced to finite numbers (gh-2662)\n\tvar type = jQuery.type( obj );\n\treturn ( type === \"number\" || type === \"string\" ) &&\n\n\t\t// parseFloat NaNs numeric-cast false positives (\"\")\n\t\t// ...but misinterprets leading-number strings, particularly hex literals (\"0x...\")\n\t\t// subtraction forces infinities to NaN\n\t\t!isNaN( obj - parseFloat( obj ) );\n};\n\njQuery.trim = function( text ) {\n\treturn text == null ?\n\t\t\"\" :\n\t\t( text + \"\" ).replace( rtrim, \"$1\" );\n};\n\n\n\n// Register as a named AMD module, since jQuery can be concatenated with other\n// files that may use define, but not via a proper concatenation script that\n// understands anonymous AMD modules. A named AMD is safest and most robust\n// way to register. Lowercase jquery is used because AMD module names are\n// derived from file names, and jQuery is normally delivered in a lowercase\n// file name. Do this after creating the global so that if an AMD module wants\n// to call noConflict to hide this version of jQuery, it will work.\n\n// Note that for maximum portability, libraries that are not jQuery should\n// declare themselves as anonymous modules, and avoid setting a global if an\n// AMD loader is present. jQuery is a special case. For more information, see\n// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon\n\nif ( typeof define === \"function\" && define.amd ) {\n\tdefine( \"jquery\", [], function() {\n\t\treturn jQuery;\n\t} );\n}\n\n\n\n\nvar\n\n\t// Map over jQuery in case of overwrite\n\t_jQuery = window.jQuery,\n\n\t// Map over the $ in case of overwrite\n\t_$ = window.$;\n\njQuery.noConflict = function( deep ) {\n\tif ( window.$ === jQuery ) {\n\t\twindow.$ = _$;\n\t}\n\n\tif ( deep && window.jQuery === jQuery ) {\n\t\twindow.jQuery = _jQuery;\n\t}\n\n\treturn jQuery;\n};\n\n// Expose jQuery and $ identifiers, even in AMD\n// (trac-7102#comment:10, https://github.com/jquery/jquery/pull/557)\n// and CommonJS for browser emulators (trac-13566)\nif ( typeof noGlobal === \"undefined\" ) {\n\twindow.jQuery = window.$ = jQuery;\n}\n\n\n\n\nreturn jQuery;\n} );", "/**\n * @popperjs/core v2.11.8 - MIT License\n */\n\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n  typeof define === 'function' && define.amd ? define(['exports'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Popper = {}));\n}(this, (function (exports) { 'use strict';\n\n  function getWindow(node) {\n    if (node == null) {\n      return window;\n    }\n\n    if (node.toString() !== '[object Window]') {\n      var ownerDocument = node.ownerDocument;\n      return ownerDocument ? ownerDocument.defaultView || window : window;\n    }\n\n    return node;\n  }\n\n  function isElement(node) {\n    var OwnElement = getWindow(node).Element;\n    return node instanceof OwnElement || node instanceof Element;\n  }\n\n  function isHTMLElement(node) {\n    var OwnElement = getWindow(node).HTMLElement;\n    return node instanceof OwnElement || node instanceof HTMLElement;\n  }\n\n  function isShadowRoot(node) {\n    // IE 11 has no ShadowRoot\n    if (typeof ShadowRoot === 'undefined') {\n      return false;\n    }\n\n    var OwnElement = getWindow(node).ShadowRoot;\n    return node instanceof OwnElement || node instanceof ShadowRoot;\n  }\n\n  var max = Math.max;\n  var min = Math.min;\n  var round = Math.round;\n\n  function getUAString() {\n    var uaData = navigator.userAgentData;\n\n    if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n      return uaData.brands.map(function (item) {\n        return item.brand + \"/\" + item.version;\n      }).join(' ');\n    }\n\n    return navigator.userAgent;\n  }\n\n  function isLayoutViewport() {\n    return !/^((?!chrome|android).)*safari/i.test(getUAString());\n  }\n\n  function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n    if (includeScale === void 0) {\n      includeScale = false;\n    }\n\n    if (isFixedStrategy === void 0) {\n      isFixedStrategy = false;\n    }\n\n    var clientRect = element.getBoundingClientRect();\n    var scaleX = 1;\n    var scaleY = 1;\n\n    if (includeScale && isHTMLElement(element)) {\n      scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n      scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n    }\n\n    var _ref = isElement(element) ? getWindow(element) : window,\n        visualViewport = _ref.visualViewport;\n\n    var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n    var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n    var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n    var width = clientRect.width / scaleX;\n    var height = clientRect.height / scaleY;\n    return {\n      width: width,\n      height: height,\n      top: y,\n      right: x + width,\n      bottom: y + height,\n      left: x,\n      x: x,\n      y: y\n    };\n  }\n\n  function getWindowScroll(node) {\n    var win = getWindow(node);\n    var scrollLeft = win.pageXOffset;\n    var scrollTop = win.pageYOffset;\n    return {\n      scrollLeft: scrollLeft,\n      scrollTop: scrollTop\n    };\n  }\n\n  function getHTMLElementScroll(element) {\n    return {\n      scrollLeft: element.scrollLeft,\n      scrollTop: element.scrollTop\n    };\n  }\n\n  function getNodeScroll(node) {\n    if (node === getWindow(node) || !isHTMLElement(node)) {\n      return getWindowScroll(node);\n    } else {\n      return getHTMLElementScroll(node);\n    }\n  }\n\n  function getNodeName(element) {\n    return element ? (element.nodeName || '').toLowerCase() : null;\n  }\n\n  function getDocumentElement(element) {\n    // $FlowFixMe[incompatible-return]: assume body is always available\n    return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n    element.document) || window.document).documentElement;\n  }\n\n  function getWindowScrollBarX(element) {\n    // If <html> has a CSS width greater than the viewport, then this will be\n    // incorrect for RTL.\n    // Popper 1 is broken in this case and never had a bug report so let's assume\n    // it's not an issue. I don't think anyone ever specifies width on <html>\n    // anyway.\n    // Browsers where the left scrollbar doesn't cause an issue report `0` for\n    // this (e.g. Edge 2019, IE11, Safari)\n    return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n  }\n\n  function getComputedStyle(element) {\n    return getWindow(element).getComputedStyle(element);\n  }\n\n  function isScrollParent(element) {\n    // Firefox wants us to check `-x` and `-y` variations as well\n    var _getComputedStyle = getComputedStyle(element),\n        overflow = _getComputedStyle.overflow,\n        overflowX = _getComputedStyle.overflowX,\n        overflowY = _getComputedStyle.overflowY;\n\n    return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n  }\n\n  function isElementScaled(element) {\n    var rect = element.getBoundingClientRect();\n    var scaleX = round(rect.width) / element.offsetWidth || 1;\n    var scaleY = round(rect.height) / element.offsetHeight || 1;\n    return scaleX !== 1 || scaleY !== 1;\n  } // Returns the composite rect of an element relative to its offsetParent.\n  // Composite means it takes into account transforms as well as layout.\n\n\n  function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n    if (isFixed === void 0) {\n      isFixed = false;\n    }\n\n    var isOffsetParentAnElement = isHTMLElement(offsetParent);\n    var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n    var documentElement = getDocumentElement(offsetParent);\n    var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n    var scroll = {\n      scrollLeft: 0,\n      scrollTop: 0\n    };\n    var offsets = {\n      x: 0,\n      y: 0\n    };\n\n    if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n      if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n      isScrollParent(documentElement)) {\n        scroll = getNodeScroll(offsetParent);\n      }\n\n      if (isHTMLElement(offsetParent)) {\n        offsets = getBoundingClientRect(offsetParent, true);\n        offsets.x += offsetParent.clientLeft;\n        offsets.y += offsetParent.clientTop;\n      } else if (documentElement) {\n        offsets.x = getWindowScrollBarX(documentElement);\n      }\n    }\n\n    return {\n      x: rect.left + scroll.scrollLeft - offsets.x,\n      y: rect.top + scroll.scrollTop - offsets.y,\n      width: rect.width,\n      height: rect.height\n    };\n  }\n\n  // means it doesn't take into account transforms.\n\n  function getLayoutRect(element) {\n    var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n    // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n    var width = element.offsetWidth;\n    var height = element.offsetHeight;\n\n    if (Math.abs(clientRect.width - width) <= 1) {\n      width = clientRect.width;\n    }\n\n    if (Math.abs(clientRect.height - height) <= 1) {\n      height = clientRect.height;\n    }\n\n    return {\n      x: element.offsetLeft,\n      y: element.offsetTop,\n      width: width,\n      height: height\n    };\n  }\n\n  function getParentNode(element) {\n    if (getNodeName(element) === 'html') {\n      return element;\n    }\n\n    return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n      // $FlowFixMe[incompatible-return]\n      // $FlowFixMe[prop-missing]\n      element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n      element.parentNode || ( // DOM Element detected\n      isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n      // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n      getDocumentElement(element) // fallback\n\n    );\n  }\n\n  function getScrollParent(node) {\n    if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n      // $FlowFixMe[incompatible-return]: assume body is always available\n      return node.ownerDocument.body;\n    }\n\n    if (isHTMLElement(node) && isScrollParent(node)) {\n      return node;\n    }\n\n    return getScrollParent(getParentNode(node));\n  }\n\n  /*\n  given a DOM element, return the list of all scroll parents, up the list of ancesors\n  until we get to the top window object. This list is what we attach scroll listeners\n  to, because if any of these parent elements scroll, we'll need to re-calculate the\n  reference element's position.\n  */\n\n  function listScrollParents(element, list) {\n    var _element$ownerDocumen;\n\n    if (list === void 0) {\n      list = [];\n    }\n\n    var scrollParent = getScrollParent(element);\n    var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n    var win = getWindow(scrollParent);\n    var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n    var updatedList = list.concat(target);\n    return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n    updatedList.concat(listScrollParents(getParentNode(target)));\n  }\n\n  function isTableElement(element) {\n    return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n  }\n\n  function getTrueOffsetParent(element) {\n    if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n    getComputedStyle(element).position === 'fixed') {\n      return null;\n    }\n\n    return element.offsetParent;\n  } // `.offsetParent` reports `null` for fixed elements, while absolute elements\n  // return the containing block\n\n\n  function getContainingBlock(element) {\n    var isFirefox = /firefox/i.test(getUAString());\n    var isIE = /Trident/i.test(getUAString());\n\n    if (isIE && isHTMLElement(element)) {\n      // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n      var elementCss = getComputedStyle(element);\n\n      if (elementCss.position === 'fixed') {\n        return null;\n      }\n    }\n\n    var currentNode = getParentNode(element);\n\n    if (isShadowRoot(currentNode)) {\n      currentNode = currentNode.host;\n    }\n\n    while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n      var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n      // create a containing block.\n      // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n      if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n        return currentNode;\n      } else {\n        currentNode = currentNode.parentNode;\n      }\n    }\n\n    return null;\n  } // Gets the closest ancestor positioned element. Handles some edge cases,\n  // such as table ancestors and cross browser bugs.\n\n\n  function getOffsetParent(element) {\n    var window = getWindow(element);\n    var offsetParent = getTrueOffsetParent(element);\n\n    while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n      offsetParent = getTrueOffsetParent(offsetParent);\n    }\n\n    if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n      return window;\n    }\n\n    return offsetParent || getContainingBlock(element) || window;\n  }\n\n  var top = 'top';\n  var bottom = 'bottom';\n  var right = 'right';\n  var left = 'left';\n  var auto = 'auto';\n  var basePlacements = [top, bottom, right, left];\n  var start = 'start';\n  var end = 'end';\n  var clippingParents = 'clippingParents';\n  var viewport = 'viewport';\n  var popper = 'popper';\n  var reference = 'reference';\n  var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n    return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n  }, []);\n  var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n    return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n  }, []); // modifiers that need to read the DOM\n\n  var beforeRead = 'beforeRead';\n  var read = 'read';\n  var afterRead = 'afterRead'; // pure-logic modifiers\n\n  var beforeMain = 'beforeMain';\n  var main = 'main';\n  var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\n  var beforeWrite = 'beforeWrite';\n  var write = 'write';\n  var afterWrite = 'afterWrite';\n  var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];\n\n  function order(modifiers) {\n    var map = new Map();\n    var visited = new Set();\n    var result = [];\n    modifiers.forEach(function (modifier) {\n      map.set(modifier.name, modifier);\n    }); // On visiting object, check for its dependencies and visit them recursively\n\n    function sort(modifier) {\n      visited.add(modifier.name);\n      var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n      requires.forEach(function (dep) {\n        if (!visited.has(dep)) {\n          var depModifier = map.get(dep);\n\n          if (depModifier) {\n            sort(depModifier);\n          }\n        }\n      });\n      result.push(modifier);\n    }\n\n    modifiers.forEach(function (modifier) {\n      if (!visited.has(modifier.name)) {\n        // check for visited object\n        sort(modifier);\n      }\n    });\n    return result;\n  }\n\n  function orderModifiers(modifiers) {\n    // order based on dependencies\n    var orderedModifiers = order(modifiers); // order based on phase\n\n    return modifierPhases.reduce(function (acc, phase) {\n      return acc.concat(orderedModifiers.filter(function (modifier) {\n        return modifier.phase === phase;\n      }));\n    }, []);\n  }\n\n  function debounce(fn) {\n    var pending;\n    return function () {\n      if (!pending) {\n        pending = new Promise(function (resolve) {\n          Promise.resolve().then(function () {\n            pending = undefined;\n            resolve(fn());\n          });\n        });\n      }\n\n      return pending;\n    };\n  }\n\n  function mergeByName(modifiers) {\n    var merged = modifiers.reduce(function (merged, current) {\n      var existing = merged[current.name];\n      merged[current.name] = existing ? Object.assign({}, existing, current, {\n        options: Object.assign({}, existing.options, current.options),\n        data: Object.assign({}, existing.data, current.data)\n      }) : current;\n      return merged;\n    }, {}); // IE11 does not support Object.values\n\n    return Object.keys(merged).map(function (key) {\n      return merged[key];\n    });\n  }\n\n  function getViewportRect(element, strategy) {\n    var win = getWindow(element);\n    var html = getDocumentElement(element);\n    var visualViewport = win.visualViewport;\n    var width = html.clientWidth;\n    var height = html.clientHeight;\n    var x = 0;\n    var y = 0;\n\n    if (visualViewport) {\n      width = visualViewport.width;\n      height = visualViewport.height;\n      var layoutViewport = isLayoutViewport();\n\n      if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n        x = visualViewport.offsetLeft;\n        y = visualViewport.offsetTop;\n      }\n    }\n\n    return {\n      width: width,\n      height: height,\n      x: x + getWindowScrollBarX(element),\n      y: y\n    };\n  }\n\n  // of the `<html>` and `<body>` rect bounds if horizontally scrollable\n\n  function getDocumentRect(element) {\n    var _element$ownerDocumen;\n\n    var html = getDocumentElement(element);\n    var winScroll = getWindowScroll(element);\n    var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n    var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n    var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n    var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n    var y = -winScroll.scrollTop;\n\n    if (getComputedStyle(body || html).direction === 'rtl') {\n      x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n    }\n\n    return {\n      width: width,\n      height: height,\n      x: x,\n      y: y\n    };\n  }\n\n  function contains(parent, child) {\n    var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n    if (parent.contains(child)) {\n      return true;\n    } // then fallback to custom implementation with Shadow DOM support\n    else if (rootNode && isShadowRoot(rootNode)) {\n        var next = child;\n\n        do {\n          if (next && parent.isSameNode(next)) {\n            return true;\n          } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n          next = next.parentNode || next.host;\n        } while (next);\n      } // Give up, the result is false\n\n\n    return false;\n  }\n\n  function rectToClientRect(rect) {\n    return Object.assign({}, rect, {\n      left: rect.x,\n      top: rect.y,\n      right: rect.x + rect.width,\n      bottom: rect.y + rect.height\n    });\n  }\n\n  function getInnerBoundingClientRect(element, strategy) {\n    var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n    rect.top = rect.top + element.clientTop;\n    rect.left = rect.left + element.clientLeft;\n    rect.bottom = rect.top + element.clientHeight;\n    rect.right = rect.left + element.clientWidth;\n    rect.width = element.clientWidth;\n    rect.height = element.clientHeight;\n    rect.x = rect.left;\n    rect.y = rect.top;\n    return rect;\n  }\n\n  function getClientRectFromMixedType(element, clippingParent, strategy) {\n    return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n  } // A \"clipping parent\" is an overflowable container with the characteristic of\n  // clipping (or hiding) overflowing elements with a position different from\n  // `initial`\n\n\n  function getClippingParents(element) {\n    var clippingParents = listScrollParents(getParentNode(element));\n    var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n    var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n    if (!isElement(clipperElement)) {\n      return [];\n    } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n    return clippingParents.filter(function (clippingParent) {\n      return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n    });\n  } // Gets the maximum area that the element is visible in due to any number of\n  // clipping parents\n\n\n  function getClippingRect(element, boundary, rootBoundary, strategy) {\n    var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n    var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n    var firstClippingParent = clippingParents[0];\n    var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n      var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n      accRect.top = max(rect.top, accRect.top);\n      accRect.right = min(rect.right, accRect.right);\n      accRect.bottom = min(rect.bottom, accRect.bottom);\n      accRect.left = max(rect.left, accRect.left);\n      return accRect;\n    }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n    clippingRect.width = clippingRect.right - clippingRect.left;\n    clippingRect.height = clippingRect.bottom - clippingRect.top;\n    clippingRect.x = clippingRect.left;\n    clippingRect.y = clippingRect.top;\n    return clippingRect;\n  }\n\n  function getBasePlacement(placement) {\n    return placement.split('-')[0];\n  }\n\n  function getVariation(placement) {\n    return placement.split('-')[1];\n  }\n\n  function getMainAxisFromPlacement(placement) {\n    return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n  }\n\n  function computeOffsets(_ref) {\n    var reference = _ref.reference,\n        element = _ref.element,\n        placement = _ref.placement;\n    var basePlacement = placement ? getBasePlacement(placement) : null;\n    var variation = placement ? getVariation(placement) : null;\n    var commonX = reference.x + reference.width / 2 - element.width / 2;\n    var commonY = reference.y + reference.height / 2 - element.height / 2;\n    var offsets;\n\n    switch (basePlacement) {\n      case top:\n        offsets = {\n          x: commonX,\n          y: reference.y - element.height\n        };\n        break;\n\n      case bottom:\n        offsets = {\n          x: commonX,\n          y: reference.y + reference.height\n        };\n        break;\n\n      case right:\n        offsets = {\n          x: reference.x + reference.width,\n          y: commonY\n        };\n        break;\n\n      case left:\n        offsets = {\n          x: reference.x - element.width,\n          y: commonY\n        };\n        break;\n\n      default:\n        offsets = {\n          x: reference.x,\n          y: reference.y\n        };\n    }\n\n    var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n    if (mainAxis != null) {\n      var len = mainAxis === 'y' ? 'height' : 'width';\n\n      switch (variation) {\n        case start:\n          offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n          break;\n\n        case end:\n          offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n          break;\n      }\n    }\n\n    return offsets;\n  }\n\n  function getFreshSideObject() {\n    return {\n      top: 0,\n      right: 0,\n      bottom: 0,\n      left: 0\n    };\n  }\n\n  function mergePaddingObject(paddingObject) {\n    return Object.assign({}, getFreshSideObject(), paddingObject);\n  }\n\n  function expandToHashMap(value, keys) {\n    return keys.reduce(function (hashMap, key) {\n      hashMap[key] = value;\n      return hashMap;\n    }, {});\n  }\n\n  function detectOverflow(state, options) {\n    if (options === void 0) {\n      options = {};\n    }\n\n    var _options = options,\n        _options$placement = _options.placement,\n        placement = _options$placement === void 0 ? state.placement : _options$placement,\n        _options$strategy = _options.strategy,\n        strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n        _options$boundary = _options.boundary,\n        boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n        _options$rootBoundary = _options.rootBoundary,\n        rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n        _options$elementConte = _options.elementContext,\n        elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n        _options$altBoundary = _options.altBoundary,\n        altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n        _options$padding = _options.padding,\n        padding = _options$padding === void 0 ? 0 : _options$padding;\n    var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n    var altContext = elementContext === popper ? reference : popper;\n    var popperRect = state.rects.popper;\n    var element = state.elements[altBoundary ? altContext : elementContext];\n    var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n    var referenceClientRect = getBoundingClientRect(state.elements.reference);\n    var popperOffsets = computeOffsets({\n      reference: referenceClientRect,\n      element: popperRect,\n      strategy: 'absolute',\n      placement: placement\n    });\n    var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n    var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n    // 0 or negative = within the clipping rect\n\n    var overflowOffsets = {\n      top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n      bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n      left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n      right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n    };\n    var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n    if (elementContext === popper && offsetData) {\n      var offset = offsetData[placement];\n      Object.keys(overflowOffsets).forEach(function (key) {\n        var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n        var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n        overflowOffsets[key] += offset[axis] * multiply;\n      });\n    }\n\n    return overflowOffsets;\n  }\n\n  var DEFAULT_OPTIONS = {\n    placement: 'bottom',\n    modifiers: [],\n    strategy: 'absolute'\n  };\n\n  function areValidElements() {\n    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n      args[_key] = arguments[_key];\n    }\n\n    return !args.some(function (element) {\n      return !(element && typeof element.getBoundingClientRect === 'function');\n    });\n  }\n\n  function popperGenerator(generatorOptions) {\n    if (generatorOptions === void 0) {\n      generatorOptions = {};\n    }\n\n    var _generatorOptions = generatorOptions,\n        _generatorOptions$def = _generatorOptions.defaultModifiers,\n        defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n        _generatorOptions$def2 = _generatorOptions.defaultOptions,\n        defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n    return function createPopper(reference, popper, options) {\n      if (options === void 0) {\n        options = defaultOptions;\n      }\n\n      var state = {\n        placement: 'bottom',\n        orderedModifiers: [],\n        options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n        modifiersData: {},\n        elements: {\n          reference: reference,\n          popper: popper\n        },\n        attributes: {},\n        styles: {}\n      };\n      var effectCleanupFns = [];\n      var isDestroyed = false;\n      var instance = {\n        state: state,\n        setOptions: function setOptions(setOptionsAction) {\n          var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n          cleanupModifierEffects();\n          state.options = Object.assign({}, defaultOptions, state.options, options);\n          state.scrollParents = {\n            reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n            popper: listScrollParents(popper)\n          }; // Orders the modifiers based on their dependencies and `phase`\n          // properties\n\n          var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n          state.orderedModifiers = orderedModifiers.filter(function (m) {\n            return m.enabled;\n          });\n          runModifierEffects();\n          return instance.update();\n        },\n        // Sync update \u2013 it will always be executed, even if not necessary. This\n        // is useful for low frequency updates where sync behavior simplifies the\n        // logic.\n        // For high frequency updates (e.g. `resize` and `scroll` events), always\n        // prefer the async Popper#update method\n        forceUpdate: function forceUpdate() {\n          if (isDestroyed) {\n            return;\n          }\n\n          var _state$elements = state.elements,\n              reference = _state$elements.reference,\n              popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n          // anymore\n\n          if (!areValidElements(reference, popper)) {\n            return;\n          } // Store the reference and popper rects to be read by modifiers\n\n\n          state.rects = {\n            reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n            popper: getLayoutRect(popper)\n          }; // Modifiers have the ability to reset the current update cycle. The\n          // most common use case for this is the `flip` modifier changing the\n          // placement, which then needs to re-run all the modifiers, because the\n          // logic was previously ran for the previous placement and is therefore\n          // stale/incorrect\n\n          state.reset = false;\n          state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n          // is filled with the initial data specified by the modifier. This means\n          // it doesn't persist and is fresh on each update.\n          // To ensure persistent data, use `${name}#persistent`\n\n          state.orderedModifiers.forEach(function (modifier) {\n            return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n          });\n\n          for (var index = 0; index < state.orderedModifiers.length; index++) {\n            if (state.reset === true) {\n              state.reset = false;\n              index = -1;\n              continue;\n            }\n\n            var _state$orderedModifie = state.orderedModifiers[index],\n                fn = _state$orderedModifie.fn,\n                _state$orderedModifie2 = _state$orderedModifie.options,\n                _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n                name = _state$orderedModifie.name;\n\n            if (typeof fn === 'function') {\n              state = fn({\n                state: state,\n                options: _options,\n                name: name,\n                instance: instance\n              }) || state;\n            }\n          }\n        },\n        // Async and optimistically optimized update \u2013 it will not be executed if\n        // not necessary (debounced to run at most once-per-tick)\n        update: debounce(function () {\n          return new Promise(function (resolve) {\n            instance.forceUpdate();\n            resolve(state);\n          });\n        }),\n        destroy: function destroy() {\n          cleanupModifierEffects();\n          isDestroyed = true;\n        }\n      };\n\n      if (!areValidElements(reference, popper)) {\n        return instance;\n      }\n\n      instance.setOptions(options).then(function (state) {\n        if (!isDestroyed && options.onFirstUpdate) {\n          options.onFirstUpdate(state);\n        }\n      }); // Modifiers have the ability to execute arbitrary code before the first\n      // update cycle runs. They will be executed in the same order as the update\n      // cycle. This is useful when a modifier adds some persistent data that\n      // other modifiers need to use, but the modifier is run after the dependent\n      // one.\n\n      function runModifierEffects() {\n        state.orderedModifiers.forEach(function (_ref) {\n          var name = _ref.name,\n              _ref$options = _ref.options,\n              options = _ref$options === void 0 ? {} : _ref$options,\n              effect = _ref.effect;\n\n          if (typeof effect === 'function') {\n            var cleanupFn = effect({\n              state: state,\n              name: name,\n              instance: instance,\n              options: options\n            });\n\n            var noopFn = function noopFn() {};\n\n            effectCleanupFns.push(cleanupFn || noopFn);\n          }\n        });\n      }\n\n      function cleanupModifierEffects() {\n        effectCleanupFns.forEach(function (fn) {\n          return fn();\n        });\n        effectCleanupFns = [];\n      }\n\n      return instance;\n    };\n  }\n\n  var passive = {\n    passive: true\n  };\n\n  function effect$2(_ref) {\n    var state = _ref.state,\n        instance = _ref.instance,\n        options = _ref.options;\n    var _options$scroll = options.scroll,\n        scroll = _options$scroll === void 0 ? true : _options$scroll,\n        _options$resize = options.resize,\n        resize = _options$resize === void 0 ? true : _options$resize;\n    var window = getWindow(state.elements.popper);\n    var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n    if (scroll) {\n      scrollParents.forEach(function (scrollParent) {\n        scrollParent.addEventListener('scroll', instance.update, passive);\n      });\n    }\n\n    if (resize) {\n      window.addEventListener('resize', instance.update, passive);\n    }\n\n    return function () {\n      if (scroll) {\n        scrollParents.forEach(function (scrollParent) {\n          scrollParent.removeEventListener('scroll', instance.update, passive);\n        });\n      }\n\n      if (resize) {\n        window.removeEventListener('resize', instance.update, passive);\n      }\n    };\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var eventListeners = {\n    name: 'eventListeners',\n    enabled: true,\n    phase: 'write',\n    fn: function fn() {},\n    effect: effect$2,\n    data: {}\n  };\n\n  function popperOffsets(_ref) {\n    var state = _ref.state,\n        name = _ref.name;\n    // Offsets are the actual position the popper needs to have to be\n    // properly positioned near its reference element\n    // This is the most basic placement, and will be adjusted by\n    // the modifiers in the next step\n    state.modifiersData[name] = computeOffsets({\n      reference: state.rects.reference,\n      element: state.rects.popper,\n      strategy: 'absolute',\n      placement: state.placement\n    });\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var popperOffsets$1 = {\n    name: 'popperOffsets',\n    enabled: true,\n    phase: 'read',\n    fn: popperOffsets,\n    data: {}\n  };\n\n  var unsetSides = {\n    top: 'auto',\n    right: 'auto',\n    bottom: 'auto',\n    left: 'auto'\n  }; // Round the offsets to the nearest suitable subpixel based on the DPR.\n  // Zooming can change the DPR, but it seems to report a value that will\n  // cleanly divide the values into the appropriate subpixels.\n\n  function roundOffsetsByDPR(_ref, win) {\n    var x = _ref.x,\n        y = _ref.y;\n    var dpr = win.devicePixelRatio || 1;\n    return {\n      x: round(x * dpr) / dpr || 0,\n      y: round(y * dpr) / dpr || 0\n    };\n  }\n\n  function mapToStyles(_ref2) {\n    var _Object$assign2;\n\n    var popper = _ref2.popper,\n        popperRect = _ref2.popperRect,\n        placement = _ref2.placement,\n        variation = _ref2.variation,\n        offsets = _ref2.offsets,\n        position = _ref2.position,\n        gpuAcceleration = _ref2.gpuAcceleration,\n        adaptive = _ref2.adaptive,\n        roundOffsets = _ref2.roundOffsets,\n        isFixed = _ref2.isFixed;\n    var _offsets$x = offsets.x,\n        x = _offsets$x === void 0 ? 0 : _offsets$x,\n        _offsets$y = offsets.y,\n        y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n    var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n      x: x,\n      y: y\n    }) : {\n      x: x,\n      y: y\n    };\n\n    x = _ref3.x;\n    y = _ref3.y;\n    var hasX = offsets.hasOwnProperty('x');\n    var hasY = offsets.hasOwnProperty('y');\n    var sideX = left;\n    var sideY = top;\n    var win = window;\n\n    if (adaptive) {\n      var offsetParent = getOffsetParent(popper);\n      var heightProp = 'clientHeight';\n      var widthProp = 'clientWidth';\n\n      if (offsetParent === getWindow(popper)) {\n        offsetParent = getDocumentElement(popper);\n\n        if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n          heightProp = 'scrollHeight';\n          widthProp = 'scrollWidth';\n        }\n      } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n      offsetParent = offsetParent;\n\n      if (placement === top || (placement === left || placement === right) && variation === end) {\n        sideY = bottom;\n        var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n        offsetParent[heightProp];\n        y -= offsetY - popperRect.height;\n        y *= gpuAcceleration ? 1 : -1;\n      }\n\n      if (placement === left || (placement === top || placement === bottom) && variation === end) {\n        sideX = right;\n        var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n        offsetParent[widthProp];\n        x -= offsetX - popperRect.width;\n        x *= gpuAcceleration ? 1 : -1;\n      }\n    }\n\n    var commonStyles = Object.assign({\n      position: position\n    }, adaptive && unsetSides);\n\n    var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n      x: x,\n      y: y\n    }, getWindow(popper)) : {\n      x: x,\n      y: y\n    };\n\n    x = _ref4.x;\n    y = _ref4.y;\n\n    if (gpuAcceleration) {\n      var _Object$assign;\n\n      return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n    }\n\n    return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n  }\n\n  function computeStyles(_ref5) {\n    var state = _ref5.state,\n        options = _ref5.options;\n    var _options$gpuAccelerat = options.gpuAcceleration,\n        gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n        _options$adaptive = options.adaptive,\n        adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n        _options$roundOffsets = options.roundOffsets,\n        roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n    var commonStyles = {\n      placement: getBasePlacement(state.placement),\n      variation: getVariation(state.placement),\n      popper: state.elements.popper,\n      popperRect: state.rects.popper,\n      gpuAcceleration: gpuAcceleration,\n      isFixed: state.options.strategy === 'fixed'\n    };\n\n    if (state.modifiersData.popperOffsets != null) {\n      state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n        offsets: state.modifiersData.popperOffsets,\n        position: state.options.strategy,\n        adaptive: adaptive,\n        roundOffsets: roundOffsets\n      })));\n    }\n\n    if (state.modifiersData.arrow != null) {\n      state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n        offsets: state.modifiersData.arrow,\n        position: 'absolute',\n        adaptive: false,\n        roundOffsets: roundOffsets\n      })));\n    }\n\n    state.attributes.popper = Object.assign({}, state.attributes.popper, {\n      'data-popper-placement': state.placement\n    });\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var computeStyles$1 = {\n    name: 'computeStyles',\n    enabled: true,\n    phase: 'beforeWrite',\n    fn: computeStyles,\n    data: {}\n  };\n\n  // and applies them to the HTMLElements such as popper and arrow\n\n  function applyStyles(_ref) {\n    var state = _ref.state;\n    Object.keys(state.elements).forEach(function (name) {\n      var style = state.styles[name] || {};\n      var attributes = state.attributes[name] || {};\n      var element = state.elements[name]; // arrow is optional + virtual elements\n\n      if (!isHTMLElement(element) || !getNodeName(element)) {\n        return;\n      } // Flow doesn't support to extend this property, but it's the most\n      // effective way to apply styles to an HTMLElement\n      // $FlowFixMe[cannot-write]\n\n\n      Object.assign(element.style, style);\n      Object.keys(attributes).forEach(function (name) {\n        var value = attributes[name];\n\n        if (value === false) {\n          element.removeAttribute(name);\n        } else {\n          element.setAttribute(name, value === true ? '' : value);\n        }\n      });\n    });\n  }\n\n  function effect$1(_ref2) {\n    var state = _ref2.state;\n    var initialStyles = {\n      popper: {\n        position: state.options.strategy,\n        left: '0',\n        top: '0',\n        margin: '0'\n      },\n      arrow: {\n        position: 'absolute'\n      },\n      reference: {}\n    };\n    Object.assign(state.elements.popper.style, initialStyles.popper);\n    state.styles = initialStyles;\n\n    if (state.elements.arrow) {\n      Object.assign(state.elements.arrow.style, initialStyles.arrow);\n    }\n\n    return function () {\n      Object.keys(state.elements).forEach(function (name) {\n        var element = state.elements[name];\n        var attributes = state.attributes[name] || {};\n        var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n        var style = styleProperties.reduce(function (style, property) {\n          style[property] = '';\n          return style;\n        }, {}); // arrow is optional + virtual elements\n\n        if (!isHTMLElement(element) || !getNodeName(element)) {\n          return;\n        }\n\n        Object.assign(element.style, style);\n        Object.keys(attributes).forEach(function (attribute) {\n          element.removeAttribute(attribute);\n        });\n      });\n    };\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var applyStyles$1 = {\n    name: 'applyStyles',\n    enabled: true,\n    phase: 'write',\n    fn: applyStyles,\n    effect: effect$1,\n    requires: ['computeStyles']\n  };\n\n  function distanceAndSkiddingToXY(placement, rects, offset) {\n    var basePlacement = getBasePlacement(placement);\n    var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n    var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n      placement: placement\n    })) : offset,\n        skidding = _ref[0],\n        distance = _ref[1];\n\n    skidding = skidding || 0;\n    distance = (distance || 0) * invertDistance;\n    return [left, right].indexOf(basePlacement) >= 0 ? {\n      x: distance,\n      y: skidding\n    } : {\n      x: skidding,\n      y: distance\n    };\n  }\n\n  function offset(_ref2) {\n    var state = _ref2.state,\n        options = _ref2.options,\n        name = _ref2.name;\n    var _options$offset = options.offset,\n        offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n    var data = placements.reduce(function (acc, placement) {\n      acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n      return acc;\n    }, {});\n    var _data$state$placement = data[state.placement],\n        x = _data$state$placement.x,\n        y = _data$state$placement.y;\n\n    if (state.modifiersData.popperOffsets != null) {\n      state.modifiersData.popperOffsets.x += x;\n      state.modifiersData.popperOffsets.y += y;\n    }\n\n    state.modifiersData[name] = data;\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var offset$1 = {\n    name: 'offset',\n    enabled: true,\n    phase: 'main',\n    requires: ['popperOffsets'],\n    fn: offset\n  };\n\n  var hash$1 = {\n    left: 'right',\n    right: 'left',\n    bottom: 'top',\n    top: 'bottom'\n  };\n  function getOppositePlacement(placement) {\n    return placement.replace(/left|right|bottom|top/g, function (matched) {\n      return hash$1[matched];\n    });\n  }\n\n  var hash = {\n    start: 'end',\n    end: 'start'\n  };\n  function getOppositeVariationPlacement(placement) {\n    return placement.replace(/start|end/g, function (matched) {\n      return hash[matched];\n    });\n  }\n\n  function computeAutoPlacement(state, options) {\n    if (options === void 0) {\n      options = {};\n    }\n\n    var _options = options,\n        placement = _options.placement,\n        boundary = _options.boundary,\n        rootBoundary = _options.rootBoundary,\n        padding = _options.padding,\n        flipVariations = _options.flipVariations,\n        _options$allowedAutoP = _options.allowedAutoPlacements,\n        allowedAutoPlacements = _options$allowedAutoP === void 0 ? placements : _options$allowedAutoP;\n    var variation = getVariation(placement);\n    var placements$1 = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n      return getVariation(placement) === variation;\n    }) : basePlacements;\n    var allowedPlacements = placements$1.filter(function (placement) {\n      return allowedAutoPlacements.indexOf(placement) >= 0;\n    });\n\n    if (allowedPlacements.length === 0) {\n      allowedPlacements = placements$1;\n    } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n    var overflows = allowedPlacements.reduce(function (acc, placement) {\n      acc[placement] = detectOverflow(state, {\n        placement: placement,\n        boundary: boundary,\n        rootBoundary: rootBoundary,\n        padding: padding\n      })[getBasePlacement(placement)];\n      return acc;\n    }, {});\n    return Object.keys(overflows).sort(function (a, b) {\n      return overflows[a] - overflows[b];\n    });\n  }\n\n  function getExpandedFallbackPlacements(placement) {\n    if (getBasePlacement(placement) === auto) {\n      return [];\n    }\n\n    var oppositePlacement = getOppositePlacement(placement);\n    return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n  }\n\n  function flip(_ref) {\n    var state = _ref.state,\n        options = _ref.options,\n        name = _ref.name;\n\n    if (state.modifiersData[name]._skip) {\n      return;\n    }\n\n    var _options$mainAxis = options.mainAxis,\n        checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n        _options$altAxis = options.altAxis,\n        checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n        specifiedFallbackPlacements = options.fallbackPlacements,\n        padding = options.padding,\n        boundary = options.boundary,\n        rootBoundary = options.rootBoundary,\n        altBoundary = options.altBoundary,\n        _options$flipVariatio = options.flipVariations,\n        flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n        allowedAutoPlacements = options.allowedAutoPlacements;\n    var preferredPlacement = state.options.placement;\n    var basePlacement = getBasePlacement(preferredPlacement);\n    var isBasePlacement = basePlacement === preferredPlacement;\n    var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n    var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n      return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n        placement: placement,\n        boundary: boundary,\n        rootBoundary: rootBoundary,\n        padding: padding,\n        flipVariations: flipVariations,\n        allowedAutoPlacements: allowedAutoPlacements\n      }) : placement);\n    }, []);\n    var referenceRect = state.rects.reference;\n    var popperRect = state.rects.popper;\n    var checksMap = new Map();\n    var makeFallbackChecks = true;\n    var firstFittingPlacement = placements[0];\n\n    for (var i = 0; i < placements.length; i++) {\n      var placement = placements[i];\n\n      var _basePlacement = getBasePlacement(placement);\n\n      var isStartVariation = getVariation(placement) === start;\n      var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n      var len = isVertical ? 'width' : 'height';\n      var overflow = detectOverflow(state, {\n        placement: placement,\n        boundary: boundary,\n        rootBoundary: rootBoundary,\n        altBoundary: altBoundary,\n        padding: padding\n      });\n      var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n      if (referenceRect[len] > popperRect[len]) {\n        mainVariationSide = getOppositePlacement(mainVariationSide);\n      }\n\n      var altVariationSide = getOppositePlacement(mainVariationSide);\n      var checks = [];\n\n      if (checkMainAxis) {\n        checks.push(overflow[_basePlacement] <= 0);\n      }\n\n      if (checkAltAxis) {\n        checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n      }\n\n      if (checks.every(function (check) {\n        return check;\n      })) {\n        firstFittingPlacement = placement;\n        makeFallbackChecks = false;\n        break;\n      }\n\n      checksMap.set(placement, checks);\n    }\n\n    if (makeFallbackChecks) {\n      // `2` may be desired in some cases \u2013 research later\n      var numberOfChecks = flipVariations ? 3 : 1;\n\n      var _loop = function _loop(_i) {\n        var fittingPlacement = placements.find(function (placement) {\n          var checks = checksMap.get(placement);\n\n          if (checks) {\n            return checks.slice(0, _i).every(function (check) {\n              return check;\n            });\n          }\n        });\n\n        if (fittingPlacement) {\n          firstFittingPlacement = fittingPlacement;\n          return \"break\";\n        }\n      };\n\n      for (var _i = numberOfChecks; _i > 0; _i--) {\n        var _ret = _loop(_i);\n\n        if (_ret === \"break\") break;\n      }\n    }\n\n    if (state.placement !== firstFittingPlacement) {\n      state.modifiersData[name]._skip = true;\n      state.placement = firstFittingPlacement;\n      state.reset = true;\n    }\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var flip$1 = {\n    name: 'flip',\n    enabled: true,\n    phase: 'main',\n    fn: flip,\n    requiresIfExists: ['offset'],\n    data: {\n      _skip: false\n    }\n  };\n\n  function getAltAxis(axis) {\n    return axis === 'x' ? 'y' : 'x';\n  }\n\n  function within(min$1, value, max$1) {\n    return max(min$1, min(value, max$1));\n  }\n  function withinMaxClamp(min, value, max) {\n    var v = within(min, value, max);\n    return v > max ? max : v;\n  }\n\n  function preventOverflow(_ref) {\n    var state = _ref.state,\n        options = _ref.options,\n        name = _ref.name;\n    var _options$mainAxis = options.mainAxis,\n        checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n        _options$altAxis = options.altAxis,\n        checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n        boundary = options.boundary,\n        rootBoundary = options.rootBoundary,\n        altBoundary = options.altBoundary,\n        padding = options.padding,\n        _options$tether = options.tether,\n        tether = _options$tether === void 0 ? true : _options$tether,\n        _options$tetherOffset = options.tetherOffset,\n        tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n    var overflow = detectOverflow(state, {\n      boundary: boundary,\n      rootBoundary: rootBoundary,\n      padding: padding,\n      altBoundary: altBoundary\n    });\n    var basePlacement = getBasePlacement(state.placement);\n    var variation = getVariation(state.placement);\n    var isBasePlacement = !variation;\n    var mainAxis = getMainAxisFromPlacement(basePlacement);\n    var altAxis = getAltAxis(mainAxis);\n    var popperOffsets = state.modifiersData.popperOffsets;\n    var referenceRect = state.rects.reference;\n    var popperRect = state.rects.popper;\n    var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n      placement: state.placement\n    })) : tetherOffset;\n    var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n      mainAxis: tetherOffsetValue,\n      altAxis: tetherOffsetValue\n    } : Object.assign({\n      mainAxis: 0,\n      altAxis: 0\n    }, tetherOffsetValue);\n    var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n    var data = {\n      x: 0,\n      y: 0\n    };\n\n    if (!popperOffsets) {\n      return;\n    }\n\n    if (checkMainAxis) {\n      var _offsetModifierState$;\n\n      var mainSide = mainAxis === 'y' ? top : left;\n      var altSide = mainAxis === 'y' ? bottom : right;\n      var len = mainAxis === 'y' ? 'height' : 'width';\n      var offset = popperOffsets[mainAxis];\n      var min$1 = offset + overflow[mainSide];\n      var max$1 = offset - overflow[altSide];\n      var additive = tether ? -popperRect[len] / 2 : 0;\n      var minLen = variation === start ? referenceRect[len] : popperRect[len];\n      var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n      // outside the reference bounds\n\n      var arrowElement = state.elements.arrow;\n      var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n        width: 0,\n        height: 0\n      };\n      var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n      var arrowPaddingMin = arrowPaddingObject[mainSide];\n      var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n      // to include its full size in the calculation. If the reference is small\n      // and near the edge of a boundary, the popper can overflow even if the\n      // reference is not overflowing as well (e.g. virtual elements with no\n      // width or height)\n\n      var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n      var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n      var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n      var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n      var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n      var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n      var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n      var tetherMax = offset + maxOffset - offsetModifierValue;\n      var preventedOffset = within(tether ? min(min$1, tetherMin) : min$1, offset, tether ? max(max$1, tetherMax) : max$1);\n      popperOffsets[mainAxis] = preventedOffset;\n      data[mainAxis] = preventedOffset - offset;\n    }\n\n    if (checkAltAxis) {\n      var _offsetModifierState$2;\n\n      var _mainSide = mainAxis === 'x' ? top : left;\n\n      var _altSide = mainAxis === 'x' ? bottom : right;\n\n      var _offset = popperOffsets[altAxis];\n\n      var _len = altAxis === 'y' ? 'height' : 'width';\n\n      var _min = _offset + overflow[_mainSide];\n\n      var _max = _offset - overflow[_altSide];\n\n      var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n      var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n      var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n      var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n      var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n      popperOffsets[altAxis] = _preventedOffset;\n      data[altAxis] = _preventedOffset - _offset;\n    }\n\n    state.modifiersData[name] = data;\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var preventOverflow$1 = {\n    name: 'preventOverflow',\n    enabled: true,\n    phase: 'main',\n    fn: preventOverflow,\n    requiresIfExists: ['offset']\n  };\n\n  var toPaddingObject = function toPaddingObject(padding, state) {\n    padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n      placement: state.placement\n    })) : padding;\n    return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n  };\n\n  function arrow(_ref) {\n    var _state$modifiersData$;\n\n    var state = _ref.state,\n        name = _ref.name,\n        options = _ref.options;\n    var arrowElement = state.elements.arrow;\n    var popperOffsets = state.modifiersData.popperOffsets;\n    var basePlacement = getBasePlacement(state.placement);\n    var axis = getMainAxisFromPlacement(basePlacement);\n    var isVertical = [left, right].indexOf(basePlacement) >= 0;\n    var len = isVertical ? 'height' : 'width';\n\n    if (!arrowElement || !popperOffsets) {\n      return;\n    }\n\n    var paddingObject = toPaddingObject(options.padding, state);\n    var arrowRect = getLayoutRect(arrowElement);\n    var minProp = axis === 'y' ? top : left;\n    var maxProp = axis === 'y' ? bottom : right;\n    var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n    var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n    var arrowOffsetParent = getOffsetParent(arrowElement);\n    var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n    var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n    // outside of the popper bounds\n\n    var min = paddingObject[minProp];\n    var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n    var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n    var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n    var axisProp = axis;\n    state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n  }\n\n  function effect(_ref2) {\n    var state = _ref2.state,\n        options = _ref2.options;\n    var _options$element = options.element,\n        arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n    if (arrowElement == null) {\n      return;\n    } // CSS selector\n\n\n    if (typeof arrowElement === 'string') {\n      arrowElement = state.elements.popper.querySelector(arrowElement);\n\n      if (!arrowElement) {\n        return;\n      }\n    }\n\n    if (!contains(state.elements.popper, arrowElement)) {\n      return;\n    }\n\n    state.elements.arrow = arrowElement;\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var arrow$1 = {\n    name: 'arrow',\n    enabled: true,\n    phase: 'main',\n    fn: arrow,\n    effect: effect,\n    requires: ['popperOffsets'],\n    requiresIfExists: ['preventOverflow']\n  };\n\n  function getSideOffsets(overflow, rect, preventedOffsets) {\n    if (preventedOffsets === void 0) {\n      preventedOffsets = {\n        x: 0,\n        y: 0\n      };\n    }\n\n    return {\n      top: overflow.top - rect.height - preventedOffsets.y,\n      right: overflow.right - rect.width + preventedOffsets.x,\n      bottom: overflow.bottom - rect.height + preventedOffsets.y,\n      left: overflow.left - rect.width - preventedOffsets.x\n    };\n  }\n\n  function isAnySideFullyClipped(overflow) {\n    return [top, right, bottom, left].some(function (side) {\n      return overflow[side] >= 0;\n    });\n  }\n\n  function hide(_ref) {\n    var state = _ref.state,\n        name = _ref.name;\n    var referenceRect = state.rects.reference;\n    var popperRect = state.rects.popper;\n    var preventedOffsets = state.modifiersData.preventOverflow;\n    var referenceOverflow = detectOverflow(state, {\n      elementContext: 'reference'\n    });\n    var popperAltOverflow = detectOverflow(state, {\n      altBoundary: true\n    });\n    var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n    var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n    var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n    var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n    state.modifiersData[name] = {\n      referenceClippingOffsets: referenceClippingOffsets,\n      popperEscapeOffsets: popperEscapeOffsets,\n      isReferenceHidden: isReferenceHidden,\n      hasPopperEscaped: hasPopperEscaped\n    };\n    state.attributes.popper = Object.assign({}, state.attributes.popper, {\n      'data-popper-reference-hidden': isReferenceHidden,\n      'data-popper-escaped': hasPopperEscaped\n    });\n  } // eslint-disable-next-line import/no-unused-modules\n\n\n  var hide$1 = {\n    name: 'hide',\n    enabled: true,\n    phase: 'main',\n    requiresIfExists: ['preventOverflow'],\n    fn: hide\n  };\n\n  var defaultModifiers$1 = [eventListeners, popperOffsets$1, computeStyles$1, applyStyles$1];\n  var createPopper$1 = /*#__PURE__*/popperGenerator({\n    defaultModifiers: defaultModifiers$1\n  }); // eslint-disable-next-line import/no-unused-modules\n\n  var defaultModifiers = [eventListeners, popperOffsets$1, computeStyles$1, applyStyles$1, offset$1, flip$1, preventOverflow$1, arrow$1, hide$1];\n  var createPopper = /*#__PURE__*/popperGenerator({\n    defaultModifiers: defaultModifiers\n  }); // eslint-disable-next-line import/no-unused-modules\n\n  exports.applyStyles = applyStyles$1;\n  exports.arrow = arrow$1;\n  exports.computeStyles = computeStyles$1;\n  exports.createPopper = createPopper;\n  exports.createPopperLite = createPopper$1;\n  exports.defaultModifiers = defaultModifiers;\n  exports.detectOverflow = detectOverflow;\n  exports.eventListeners = eventListeners;\n  exports.flip = flip$1;\n  exports.hide = hide$1;\n  exports.offset = offset$1;\n  exports.popperGenerator = popperGenerator;\n  exports.popperOffsets = popperOffsets$1;\n  exports.preventOverflow = preventOverflow$1;\n\n  Object.defineProperty(exports, '__esModule', { value: true });\n\n})));\n//# sourceMappingURL=popper.js.map\n", "/*!\n  * Bootstrap index.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n  typeof define === 'function' && define.amd ? define(['exports'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Index = {}));\n})(this, (function (exports) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/index.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  const MAX_UID = 1000000;\n  const MILLISECONDS_MULTIPLIER = 1000;\n  const TRANSITION_END = 'transitionend';\n\n  /**\n   * Properly escape IDs selectors to handle weird IDs\n   * @param {string} selector\n   * @returns {string}\n   */\n  const parseSelector = selector => {\n    if (selector && window.CSS && window.CSS.escape) {\n      // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n      selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`);\n    }\n    return selector;\n  };\n\n  // Shout-out Angus Croll (https://goo.gl/pxwQGp)\n  const toType = object => {\n    if (object === null || object === undefined) {\n      return `${object}`;\n    }\n    return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase();\n  };\n\n  /**\n   * Public Util API\n   */\n\n  const getUID = prefix => {\n    do {\n      prefix += Math.floor(Math.random() * MAX_UID);\n    } while (document.getElementById(prefix));\n    return prefix;\n  };\n  const getTransitionDurationFromElement = element => {\n    if (!element) {\n      return 0;\n    }\n\n    // Get transition-duration of the element\n    let {\n      transitionDuration,\n      transitionDelay\n    } = window.getComputedStyle(element);\n    const floatTransitionDuration = Number.parseFloat(transitionDuration);\n    const floatTransitionDelay = Number.parseFloat(transitionDelay);\n\n    // Return 0 if element or transition duration is not found\n    if (!floatTransitionDuration && !floatTransitionDelay) {\n      return 0;\n    }\n\n    // If multiple durations are defined, take the first\n    transitionDuration = transitionDuration.split(',')[0];\n    transitionDelay = transitionDelay.split(',')[0];\n    return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;\n  };\n  const triggerTransitionEnd = element => {\n    element.dispatchEvent(new Event(TRANSITION_END));\n  };\n  const isElement = object => {\n    if (!object || typeof object !== 'object') {\n      return false;\n    }\n    if (typeof object.jquery !== 'undefined') {\n      object = object[0];\n    }\n    return typeof object.nodeType !== 'undefined';\n  };\n  const getElement = object => {\n    // it's a jQuery object or a node element\n    if (isElement(object)) {\n      return object.jquery ? object[0] : object;\n    }\n    if (typeof object === 'string' && object.length > 0) {\n      return document.querySelector(parseSelector(object));\n    }\n    return null;\n  };\n  const isVisible = element => {\n    if (!isElement(element) || element.getClientRects().length === 0) {\n      return false;\n    }\n    const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';\n    // Handle `details` element as its content may falsie appear visible when it is closed\n    const closedDetails = element.closest('details:not([open])');\n    if (!closedDetails) {\n      return elementIsVisible;\n    }\n    if (closedDetails !== element) {\n      const summary = element.closest('summary');\n      if (summary && summary.parentNode !== closedDetails) {\n        return false;\n      }\n      if (summary === null) {\n        return false;\n      }\n    }\n    return elementIsVisible;\n  };\n  const isDisabled = element => {\n    if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n      return true;\n    }\n    if (element.classList.contains('disabled')) {\n      return true;\n    }\n    if (typeof element.disabled !== 'undefined') {\n      return element.disabled;\n    }\n    return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n  };\n  const findShadowRoot = element => {\n    if (!document.documentElement.attachShadow) {\n      return null;\n    }\n\n    // Can find the shadow root otherwise it'll return the document\n    if (typeof element.getRootNode === 'function') {\n      const root = element.getRootNode();\n      return root instanceof ShadowRoot ? root : null;\n    }\n    if (element instanceof ShadowRoot) {\n      return element;\n    }\n\n    // when we don't find a shadow root\n    if (!element.parentNode) {\n      return null;\n    }\n    return findShadowRoot(element.parentNode);\n  };\n  const noop = () => {};\n\n  /**\n   * Trick to restart an element's animation\n   *\n   * @param {HTMLElement} element\n   * @return void\n   *\n   * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n   */\n  const reflow = element => {\n    element.offsetHeight; // eslint-disable-line no-unused-expressions\n  };\n  const getjQuery = () => {\n    if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n      return window.jQuery;\n    }\n    return null;\n  };\n  const DOMContentLoadedCallbacks = [];\n  const onDOMContentLoaded = callback => {\n    if (document.readyState === 'loading') {\n      // add listener on the first call when the document is in loading state\n      if (!DOMContentLoadedCallbacks.length) {\n        document.addEventListener('DOMContentLoaded', () => {\n          for (const callback of DOMContentLoadedCallbacks) {\n            callback();\n          }\n        });\n      }\n      DOMContentLoadedCallbacks.push(callback);\n    } else {\n      callback();\n    }\n  };\n  const isRTL = () => document.documentElement.dir === 'rtl';\n  const defineJQueryPlugin = plugin => {\n    onDOMContentLoaded(() => {\n      const $ = getjQuery();\n      /* istanbul ignore if */\n      if ($) {\n        const name = plugin.NAME;\n        const JQUERY_NO_CONFLICT = $.fn[name];\n        $.fn[name] = plugin.jQueryInterface;\n        $.fn[name].Constructor = plugin;\n        $.fn[name].noConflict = () => {\n          $.fn[name] = JQUERY_NO_CONFLICT;\n          return plugin.jQueryInterface;\n        };\n      }\n    });\n  };\n  const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n    return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;\n  };\n  const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n    if (!waitForTransition) {\n      execute(callback);\n      return;\n    }\n    const durationPadding = 5;\n    const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding;\n    let called = false;\n    const handler = ({\n      target\n    }) => {\n      if (target !== transitionElement) {\n        return;\n      }\n      called = true;\n      transitionElement.removeEventListener(TRANSITION_END, handler);\n      execute(callback);\n    };\n    transitionElement.addEventListener(TRANSITION_END, handler);\n    setTimeout(() => {\n      if (!called) {\n        triggerTransitionEnd(transitionElement);\n      }\n    }, emulatedDuration);\n  };\n\n  /**\n   * Return the previous/next element of a list.\n   *\n   * @param {array} list    The list of elements\n   * @param activeElement   The active element\n   * @param shouldGetNext   Choose to get next or previous element\n   * @param isCycleAllowed\n   * @return {Element|elem} The proper element\n   */\n  const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n    const listLength = list.length;\n    let index = list.indexOf(activeElement);\n\n    // if the element does not exist in the list return an element\n    // depending on the direction and if cycle is allowed\n    if (index === -1) {\n      return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0];\n    }\n    index += shouldGetNext ? 1 : -1;\n    if (isCycleAllowed) {\n      index = (index + listLength) % listLength;\n    }\n    return list[Math.max(0, Math.min(index, listLength - 1))];\n  };\n\n  exports.defineJQueryPlugin = defineJQueryPlugin;\n  exports.execute = execute;\n  exports.executeAfterTransition = executeAfterTransition;\n  exports.findShadowRoot = findShadowRoot;\n  exports.getElement = getElement;\n  exports.getNextActiveElement = getNextActiveElement;\n  exports.getTransitionDurationFromElement = getTransitionDurationFromElement;\n  exports.getUID = getUID;\n  exports.getjQuery = getjQuery;\n  exports.isDisabled = isDisabled;\n  exports.isElement = isElement;\n  exports.isRTL = isRTL;\n  exports.isVisible = isVisible;\n  exports.noop = noop;\n  exports.onDOMContentLoaded = onDOMContentLoaded;\n  exports.parseSelector = parseSelector;\n  exports.reflow = reflow;\n  exports.toType = toType;\n  exports.triggerTransitionEnd = triggerTransitionEnd;\n\n  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\n}));\n//# sourceMappingURL=index.js.map\n", "/*!\n  * Bootstrap data.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n  typeof define === 'function' && define.amd ? define(factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Data = factory());\n})(this, (function () { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dom/data.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  /**\n   * Constants\n   */\n\n  const elementMap = new Map();\n  const data = {\n    set(element, key, instance) {\n      if (!elementMap.has(element)) {\n        elementMap.set(element, new Map());\n      }\n      const instanceMap = elementMap.get(element);\n\n      // make it clear we only want one instance per element\n      // can be removed later when multiple key/instances are fine to be used\n      if (!instanceMap.has(key) && instanceMap.size !== 0) {\n        // eslint-disable-next-line no-console\n        console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`);\n        return;\n      }\n      instanceMap.set(key, instance);\n    },\n    get(element, key) {\n      if (elementMap.has(element)) {\n        return elementMap.get(element).get(key) || null;\n      }\n      return null;\n    },\n    remove(element, key) {\n      if (!elementMap.has(element)) {\n        return;\n      }\n      const instanceMap = elementMap.get(element);\n      instanceMap.delete(key);\n\n      // free up element references if there are no instances left for an element\n      if (instanceMap.size === 0) {\n        elementMap.delete(element);\n      }\n    }\n  };\n\n  return data;\n\n}));\n//# sourceMappingURL=data.js.map\n", "/*!\n  * Bootstrap event-handler.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['../util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.EventHandler = factory(global.Index));\n})(this, (function (index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dom/event-handler.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const namespaceRegex = /[^.]*(?=\\..*)\\.|.*/;\n  const stripNameRegex = /\\..*/;\n  const stripUidRegex = /::\\d+$/;\n  const eventRegistry = {}; // Events storage\n  let uidEvent = 1;\n  const customEvents = {\n    mouseenter: 'mouseover',\n    mouseleave: 'mouseout'\n  };\n  const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']);\n\n  /**\n   * Private methods\n   */\n\n  function makeEventUid(element, uid) {\n    return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++;\n  }\n  function getElementEvents(element) {\n    const uid = makeEventUid(element);\n    element.uidEvent = uid;\n    eventRegistry[uid] = eventRegistry[uid] || {};\n    return eventRegistry[uid];\n  }\n  function bootstrapHandler(element, fn) {\n    return function handler(event) {\n      hydrateObj(event, {\n        delegateTarget: element\n      });\n      if (handler.oneOff) {\n        EventHandler.off(element, event.type, fn);\n      }\n      return fn.apply(element, [event]);\n    };\n  }\n  function bootstrapDelegationHandler(element, selector, fn) {\n    return function handler(event) {\n      const domElements = element.querySelectorAll(selector);\n      for (let {\n        target\n      } = event; target && target !== this; target = target.parentNode) {\n        for (const domElement of domElements) {\n          if (domElement !== target) {\n            continue;\n          }\n          hydrateObj(event, {\n            delegateTarget: target\n          });\n          if (handler.oneOff) {\n            EventHandler.off(element, event.type, selector, fn);\n          }\n          return fn.apply(target, [event]);\n        }\n      }\n    };\n  }\n  function findHandler(events, callable, delegationSelector = null) {\n    return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector);\n  }\n  function normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n    const isDelegated = typeof handler === 'string';\n    // TODO: tooltip passes `false` instead of selector, so we need to check\n    const callable = isDelegated ? delegationFunction : handler || delegationFunction;\n    let typeEvent = getTypeEvent(originalTypeEvent);\n    if (!nativeEvents.has(typeEvent)) {\n      typeEvent = originalTypeEvent;\n    }\n    return [isDelegated, callable, typeEvent];\n  }\n  function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n    if (typeof originalTypeEvent !== 'string' || !element) {\n      return;\n    }\n    let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n\n    // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n    // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n    if (originalTypeEvent in customEvents) {\n      const wrapFunction = fn => {\n        return function (event) {\n          if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) {\n            return fn.call(this, event);\n          }\n        };\n      };\n      callable = wrapFunction(callable);\n    }\n    const events = getElementEvents(element);\n    const handlers = events[typeEvent] || (events[typeEvent] = {});\n    const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null);\n    if (previousFunction) {\n      previousFunction.oneOff = previousFunction.oneOff && oneOff;\n      return;\n    }\n    const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''));\n    const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable);\n    fn.delegationSelector = isDelegated ? handler : null;\n    fn.callable = callable;\n    fn.oneOff = oneOff;\n    fn.uidEvent = uid;\n    handlers[uid] = fn;\n    element.addEventListener(typeEvent, fn, isDelegated);\n  }\n  function removeHandler(element, events, typeEvent, handler, delegationSelector) {\n    const fn = findHandler(events[typeEvent], handler, delegationSelector);\n    if (!fn) {\n      return;\n    }\n    element.removeEventListener(typeEvent, fn, Boolean(delegationSelector));\n    delete events[typeEvent][fn.uidEvent];\n  }\n  function removeNamespacedHandlers(element, events, typeEvent, namespace) {\n    const storeElementEvent = events[typeEvent] || {};\n    for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n      if (handlerKey.includes(namespace)) {\n        removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n      }\n    }\n  }\n  function getTypeEvent(event) {\n    // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n    event = event.replace(stripNameRegex, '');\n    return customEvents[event] || event;\n  }\n  const EventHandler = {\n    on(element, event, handler, delegationFunction) {\n      addHandler(element, event, handler, delegationFunction, false);\n    },\n    one(element, event, handler, delegationFunction) {\n      addHandler(element, event, handler, delegationFunction, true);\n    },\n    off(element, originalTypeEvent, handler, delegationFunction) {\n      if (typeof originalTypeEvent !== 'string' || !element) {\n        return;\n      }\n      const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n      const inNamespace = typeEvent !== originalTypeEvent;\n      const events = getElementEvents(element);\n      const storeElementEvent = events[typeEvent] || {};\n      const isNamespace = originalTypeEvent.startsWith('.');\n      if (typeof callable !== 'undefined') {\n        // Simplest case: handler is passed, remove that listener ONLY.\n        if (!Object.keys(storeElementEvent).length) {\n          return;\n        }\n        removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null);\n        return;\n      }\n      if (isNamespace) {\n        for (const elementEvent of Object.keys(events)) {\n          removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1));\n        }\n      }\n      for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n        const handlerKey = keyHandlers.replace(stripUidRegex, '');\n        if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n          removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n        }\n      }\n    },\n    trigger(element, event, args) {\n      if (typeof event !== 'string' || !element) {\n        return null;\n      }\n      const $ = index_js.getjQuery();\n      const typeEvent = getTypeEvent(event);\n      const inNamespace = event !== typeEvent;\n      let jQueryEvent = null;\n      let bubbles = true;\n      let nativeDispatch = true;\n      let defaultPrevented = false;\n      if (inNamespace && $) {\n        jQueryEvent = $.Event(event, args);\n        $(element).trigger(jQueryEvent);\n        bubbles = !jQueryEvent.isPropagationStopped();\n        nativeDispatch = !jQueryEvent.isImmediatePropagationStopped();\n        defaultPrevented = jQueryEvent.isDefaultPrevented();\n      }\n      const evt = hydrateObj(new Event(event, {\n        bubbles,\n        cancelable: true\n      }), args);\n      if (defaultPrevented) {\n        evt.preventDefault();\n      }\n      if (nativeDispatch) {\n        element.dispatchEvent(evt);\n      }\n      if (evt.defaultPrevented && jQueryEvent) {\n        jQueryEvent.preventDefault();\n      }\n      return evt;\n    }\n  };\n  function hydrateObj(obj, meta = {}) {\n    for (const [key, value] of Object.entries(meta)) {\n      try {\n        obj[key] = value;\n      } catch (_unused) {\n        Object.defineProperty(obj, key, {\n          configurable: true,\n          get() {\n            return value;\n          }\n        });\n      }\n    }\n    return obj;\n  }\n\n  return EventHandler;\n\n}));\n//# sourceMappingURL=event-handler.js.map\n", "/*!\n  * Bootstrap manipulator.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n  typeof define === 'function' && define.amd ? define(factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Manipulator = factory());\n})(this, (function () { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dom/manipulator.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  function normalizeData(value) {\n    if (value === 'true') {\n      return true;\n    }\n    if (value === 'false') {\n      return false;\n    }\n    if (value === Number(value).toString()) {\n      return Number(value);\n    }\n    if (value === '' || value === 'null') {\n      return null;\n    }\n    if (typeof value !== 'string') {\n      return value;\n    }\n    try {\n      return JSON.parse(decodeURIComponent(value));\n    } catch (_unused) {\n      return value;\n    }\n  }\n  function normalizeDataKey(key) {\n    return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`);\n  }\n  const Manipulator = {\n    setDataAttribute(element, key, value) {\n      element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value);\n    },\n    removeDataAttribute(element, key) {\n      element.removeAttribute(`data-bs-${normalizeDataKey(key)}`);\n    },\n    getDataAttributes(element) {\n      if (!element) {\n        return {};\n      }\n      const attributes = {};\n      const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));\n      for (const key of bsKeys) {\n        let pureKey = key.replace(/^bs/, '');\n        pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);\n        attributes[pureKey] = normalizeData(element.dataset[key]);\n      }\n      return attributes;\n    },\n    getDataAttribute(element, key) {\n      return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`));\n    }\n  };\n\n  return Manipulator;\n\n}));\n//# sourceMappingURL=manipulator.js.map\n", "/*!\n  * Bootstrap selector-engine.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['../util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.SelectorEngine = factory(global.Index));\n})(this, (function (index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dom/selector-engine.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  const getSelector = element => {\n    let selector = element.getAttribute('data-bs-target');\n    if (!selector || selector === '#') {\n      let hrefAttribute = element.getAttribute('href');\n\n      // The only valid content that could double as a selector are IDs or classes,\n      // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n      // `document.querySelector` will rightfully complain it is invalid.\n      // See https://github.com/twbs/bootstrap/issues/32273\n      if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) {\n        return null;\n      }\n\n      // Just in case some CMS puts out a full URL with the anchor appended\n      if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n        hrefAttribute = `#${hrefAttribute.split('#')[1]}`;\n      }\n      selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null;\n    }\n    return selector ? selector.split(',').map(sel => index_js.parseSelector(sel)).join(',') : null;\n  };\n  const SelectorEngine = {\n    find(selector, element = document.documentElement) {\n      return [].concat(...Element.prototype.querySelectorAll.call(element, selector));\n    },\n    findOne(selector, element = document.documentElement) {\n      return Element.prototype.querySelector.call(element, selector);\n    },\n    children(element, selector) {\n      return [].concat(...element.children).filter(child => child.matches(selector));\n    },\n    parents(element, selector) {\n      const parents = [];\n      let ancestor = element.parentNode.closest(selector);\n      while (ancestor) {\n        parents.push(ancestor);\n        ancestor = ancestor.parentNode.closest(selector);\n      }\n      return parents;\n    },\n    prev(element, selector) {\n      let previous = element.previousElementSibling;\n      while (previous) {\n        if (previous.matches(selector)) {\n          return [previous];\n        }\n        previous = previous.previousElementSibling;\n      }\n      return [];\n    },\n    // TODO: this is now unused; remove later along with prev()\n    next(element, selector) {\n      let next = element.nextElementSibling;\n      while (next) {\n        if (next.matches(selector)) {\n          return [next];\n        }\n        next = next.nextElementSibling;\n      }\n      return [];\n    },\n    focusableChildren(element) {\n      const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable=\"true\"]'].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',');\n      return this.find(focusables, element).filter(el => !index_js.isDisabled(el) && index_js.isVisible(el));\n    },\n    getSelectorFromElement(element) {\n      const selector = getSelector(element);\n      if (selector) {\n        return SelectorEngine.findOne(selector) ? selector : null;\n      }\n      return null;\n    },\n    getElementFromSelector(element) {\n      const selector = getSelector(element);\n      return selector ? SelectorEngine.findOne(selector) : null;\n    },\n    getMultipleElementsFromSelector(element) {\n      const selector = getSelector(element);\n      return selector ? SelectorEngine.find(selector) : [];\n    }\n  };\n\n  return SelectorEngine;\n\n}));\n//# sourceMappingURL=selector-engine.js.map\n", "/*!\n  * Bootstrap config.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/manipulator.js'), require('./index.js')) :\n  typeof define === 'function' && define.amd ? define(['../dom/manipulator', './index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Config = factory(global.Manipulator, global.Index));\n})(this, (function (Manipulator, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/config.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Class definition\n   */\n\n  class Config {\n    // Getters\n    static get Default() {\n      return {};\n    }\n    static get DefaultType() {\n      return {};\n    }\n    static get NAME() {\n      throw new Error('You have to implement the static method \"NAME\", for each component!');\n    }\n    _getConfig(config) {\n      config = this._mergeConfigObj(config);\n      config = this._configAfterMerge(config);\n      this._typeCheckConfig(config);\n      return config;\n    }\n    _configAfterMerge(config) {\n      return config;\n    }\n    _mergeConfigObj(config, element) {\n      const jsonConfig = index_js.isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse\n\n      return {\n        ...this.constructor.Default,\n        ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n        ...(index_js.isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n        ...(typeof config === 'object' ? config : {})\n      };\n    }\n    _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n      for (const [property, expectedTypes] of Object.entries(configTypes)) {\n        const value = config[property];\n        const valueType = index_js.isElement(value) ? 'element' : index_js.toType(value);\n        if (!new RegExp(expectedTypes).test(valueType)) {\n          throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`);\n        }\n      }\n    }\n  }\n\n  return Config;\n\n}));\n//# sourceMappingURL=config.js.map\n", "/*!\n  * Bootstrap component-functions.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('../dom/event-handler.js'), require('../dom/selector-engine.js'), require('./index.js')) :\n  typeof define === 'function' && define.amd ? define(['exports', '../dom/event-handler', '../dom/selector-engine', './index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ComponentFunctions = {}, global.EventHandler, global.SelectorEngine, global.Index));\n})(this, (function (exports, EventHandler, SelectorEngine, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/component-functions.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  const enableDismissTrigger = (component, method = 'hide') => {\n    const clickEvent = `click.dismiss${component.EVENT_KEY}`;\n    const name = component.NAME;\n    EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n      if (['A', 'AREA'].includes(this.tagName)) {\n        event.preventDefault();\n      }\n      if (index_js.isDisabled(this)) {\n        return;\n      }\n      const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`);\n      const instance = component.getOrCreateInstance(target);\n\n      // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n      instance[method]();\n    });\n  };\n\n  exports.enableDismissTrigger = enableDismissTrigger;\n\n  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\n}));\n//# sourceMappingURL=component-functions.js.map\n", "/*!\n  * Bootstrap backdrop.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/event-handler.js'), require('./config.js'), require('./index.js')) :\n  typeof define === 'function' && define.amd ? define(['../dom/event-handler', './config', './index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Backdrop = factory(global.EventHandler, global.Config, global.Index));\n})(this, (function (EventHandler, Config, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/backdrop.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'backdrop';\n  const CLASS_NAME_FADE = 'fade';\n  const CLASS_NAME_SHOW = 'show';\n  const EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`;\n  const Default = {\n    className: 'modal-backdrop',\n    clickCallback: null,\n    isAnimated: false,\n    isVisible: true,\n    // if false, we use the backdrop helper without adding any element to the dom\n    rootElement: 'body' // give the choice to place backdrop under different elements\n  };\n  const DefaultType = {\n    className: 'string',\n    clickCallback: '(function|null)',\n    isAnimated: 'boolean',\n    isVisible: 'boolean',\n    rootElement: '(element|string)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Backdrop extends Config {\n    constructor(config) {\n      super();\n      this._config = this._getConfig(config);\n      this._isAppended = false;\n      this._element = null;\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    show(callback) {\n      if (!this._config.isVisible) {\n        index_js.execute(callback);\n        return;\n      }\n      this._append();\n      const element = this._getElement();\n      if (this._config.isAnimated) {\n        index_js.reflow(element);\n      }\n      element.classList.add(CLASS_NAME_SHOW);\n      this._emulateAnimation(() => {\n        index_js.execute(callback);\n      });\n    }\n    hide(callback) {\n      if (!this._config.isVisible) {\n        index_js.execute(callback);\n        return;\n      }\n      this._getElement().classList.remove(CLASS_NAME_SHOW);\n      this._emulateAnimation(() => {\n        this.dispose();\n        index_js.execute(callback);\n      });\n    }\n    dispose() {\n      if (!this._isAppended) {\n        return;\n      }\n      EventHandler.off(this._element, EVENT_MOUSEDOWN);\n      this._element.remove();\n      this._isAppended = false;\n    }\n\n    // Private\n    _getElement() {\n      if (!this._element) {\n        const backdrop = document.createElement('div');\n        backdrop.className = this._config.className;\n        if (this._config.isAnimated) {\n          backdrop.classList.add(CLASS_NAME_FADE);\n        }\n        this._element = backdrop;\n      }\n      return this._element;\n    }\n    _configAfterMerge(config) {\n      // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n      config.rootElement = index_js.getElement(config.rootElement);\n      return config;\n    }\n    _append() {\n      if (this._isAppended) {\n        return;\n      }\n      const element = this._getElement();\n      this._config.rootElement.append(element);\n      EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n        index_js.execute(this._config.clickCallback);\n      });\n      this._isAppended = true;\n    }\n    _emulateAnimation(callback) {\n      index_js.executeAfterTransition(callback, this._getElement(), this._config.isAnimated);\n    }\n  }\n\n  return Backdrop;\n\n}));\n//# sourceMappingURL=backdrop.js.map\n", "/*!\n  * Bootstrap focustrap.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/event-handler.js'), require('../dom/selector-engine.js'), require('./config.js')) :\n  typeof define === 'function' && define.amd ? define(['../dom/event-handler', '../dom/selector-engine', './config'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Focustrap = factory(global.EventHandler, global.SelectorEngine, global.Config));\n})(this, (function (EventHandler, SelectorEngine, Config) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/focustrap.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'focustrap';\n  const DATA_KEY = 'bs.focustrap';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const EVENT_FOCUSIN = `focusin${EVENT_KEY}`;\n  const EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY}`;\n  const TAB_KEY = 'Tab';\n  const TAB_NAV_FORWARD = 'forward';\n  const TAB_NAV_BACKWARD = 'backward';\n  const Default = {\n    autofocus: true,\n    trapElement: null // The element to trap focus inside of\n  };\n  const DefaultType = {\n    autofocus: 'boolean',\n    trapElement: 'element'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class FocusTrap extends Config {\n    constructor(config) {\n      super();\n      this._config = this._getConfig(config);\n      this._isActive = false;\n      this._lastTabNavDirection = null;\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    activate() {\n      if (this._isActive) {\n        return;\n      }\n      if (this._config.autofocus) {\n        this._config.trapElement.focus();\n      }\n      EventHandler.off(document, EVENT_KEY); // guard against infinite focus loop\n      EventHandler.on(document, EVENT_FOCUSIN, event => this._handleFocusin(event));\n      EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event));\n      this._isActive = true;\n    }\n    deactivate() {\n      if (!this._isActive) {\n        return;\n      }\n      this._isActive = false;\n      EventHandler.off(document, EVENT_KEY);\n    }\n\n    // Private\n    _handleFocusin(event) {\n      const {\n        trapElement\n      } = this._config;\n      if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n        return;\n      }\n      const elements = SelectorEngine.focusableChildren(trapElement);\n      if (elements.length === 0) {\n        trapElement.focus();\n      } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n        elements[elements.length - 1].focus();\n      } else {\n        elements[0].focus();\n      }\n    }\n    _handleKeydown(event) {\n      if (event.key !== TAB_KEY) {\n        return;\n      }\n      this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD;\n    }\n  }\n\n  return FocusTrap;\n\n}));\n//# sourceMappingURL=focustrap.js.map\n", "/*!\n  * Bootstrap sanitizer.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n  typeof define === 'function' && define.amd ? define(['exports'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Sanitizer = {}));\n})(this, (function (exports) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/sanitizer.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n  // js-docs-start allow-list\n  const ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i;\n  const DefaultAllowlist = {\n    // Global attributes allowed on any supplied element below.\n    '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n    a: ['target', 'href', 'title', 'rel'],\n    area: [],\n    b: [],\n    br: [],\n    col: [],\n    code: [],\n    dd: [],\n    div: [],\n    dl: [],\n    dt: [],\n    em: [],\n    hr: [],\n    h1: [],\n    h2: [],\n    h3: [],\n    h4: [],\n    h5: [],\n    h6: [],\n    i: [],\n    img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n    li: [],\n    ol: [],\n    p: [],\n    pre: [],\n    s: [],\n    small: [],\n    span: [],\n    sub: [],\n    sup: [],\n    strong: [],\n    u: [],\n    ul: []\n  };\n  // js-docs-end allow-list\n\n  const uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']);\n\n  /**\n   * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n   * contexts.\n   *\n   * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n   */\n  // eslint-disable-next-line unicorn/better-regex\n  const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;\n  const allowedAttribute = (attribute, allowedAttributeList) => {\n    const attributeName = attribute.nodeName.toLowerCase();\n    if (allowedAttributeList.includes(attributeName)) {\n      if (uriAttributes.has(attributeName)) {\n        return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue));\n      }\n      return true;\n    }\n\n    // Check if a regular expression validates the attribute.\n    return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName));\n  };\n  function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n    if (!unsafeHtml.length) {\n      return unsafeHtml;\n    }\n    if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n      return sanitizeFunction(unsafeHtml);\n    }\n    const domParser = new window.DOMParser();\n    const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');\n    const elements = [].concat(...createdDocument.body.querySelectorAll('*'));\n    for (const element of elements) {\n      const elementName = element.nodeName.toLowerCase();\n      if (!Object.keys(allowList).includes(elementName)) {\n        element.remove();\n        continue;\n      }\n      const attributeList = [].concat(...element.attributes);\n      const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []);\n      for (const attribute of attributeList) {\n        if (!allowedAttribute(attribute, allowedAttributes)) {\n          element.removeAttribute(attribute.nodeName);\n        }\n      }\n    }\n    return createdDocument.body.innerHTML;\n  }\n\n  exports.DefaultAllowlist = DefaultAllowlist;\n  exports.sanitizeHtml = sanitizeHtml;\n\n  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\n}));\n//# sourceMappingURL=sanitizer.js.map\n", "/*!\n  * Bootstrap scrollbar.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/manipulator.js'), require('../dom/selector-engine.js'), require('./index.js')) :\n  typeof define === 'function' && define.amd ? define(['../dom/manipulator', '../dom/selector-engine', './index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Scrollbar = factory(global.Manipulator, global.SelectorEngine, global.Index));\n})(this, (function (Manipulator, SelectorEngine, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/scrollBar.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top';\n  const SELECTOR_STICKY_CONTENT = '.sticky-top';\n  const PROPERTY_PADDING = 'padding-right';\n  const PROPERTY_MARGIN = 'margin-right';\n\n  /**\n   * Class definition\n   */\n\n  class ScrollBarHelper {\n    constructor() {\n      this._element = document.body;\n    }\n\n    // Public\n    getWidth() {\n      // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n      const documentWidth = document.documentElement.clientWidth;\n      return Math.abs(window.innerWidth - documentWidth);\n    }\n    hide() {\n      const width = this.getWidth();\n      this._disableOverFlow();\n      // give padding to element to balance the hidden scrollbar width\n      this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n      // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n      this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n      this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width);\n    }\n    reset() {\n      this._resetElementAttributes(this._element, 'overflow');\n      this._resetElementAttributes(this._element, PROPERTY_PADDING);\n      this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING);\n      this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN);\n    }\n    isOverflowing() {\n      return this.getWidth() > 0;\n    }\n\n    // Private\n    _disableOverFlow() {\n      this._saveInitialAttribute(this._element, 'overflow');\n      this._element.style.overflow = 'hidden';\n    }\n    _setElementAttributes(selector, styleProperty, callback) {\n      const scrollbarWidth = this.getWidth();\n      const manipulationCallBack = element => {\n        if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n          return;\n        }\n        this._saveInitialAttribute(element, styleProperty);\n        const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty);\n        element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`);\n      };\n      this._applyManipulationCallback(selector, manipulationCallBack);\n    }\n    _saveInitialAttribute(element, styleProperty) {\n      const actualValue = element.style.getPropertyValue(styleProperty);\n      if (actualValue) {\n        Manipulator.setDataAttribute(element, styleProperty, actualValue);\n      }\n    }\n    _resetElementAttributes(selector, styleProperty) {\n      const manipulationCallBack = element => {\n        const value = Manipulator.getDataAttribute(element, styleProperty);\n        // We only want to remove the property if the value is `null`; the value can also be zero\n        if (value === null) {\n          element.style.removeProperty(styleProperty);\n          return;\n        }\n        Manipulator.removeDataAttribute(element, styleProperty);\n        element.style.setProperty(styleProperty, value);\n      };\n      this._applyManipulationCallback(selector, manipulationCallBack);\n    }\n    _applyManipulationCallback(selector, callBack) {\n      if (index_js.isElement(selector)) {\n        callBack(selector);\n        return;\n      }\n      for (const sel of SelectorEngine.find(selector, this._element)) {\n        callBack(sel);\n      }\n    }\n  }\n\n  return ScrollBarHelper;\n\n}));\n//# sourceMappingURL=scrollbar.js.map\n", "/*!\n  * Bootstrap swipe.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/event-handler.js'), require('./config.js'), require('./index.js')) :\n  typeof define === 'function' && define.amd ? define(['../dom/event-handler', './config', './index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Swipe = factory(global.EventHandler, global.Config, global.Index));\n})(this, (function (EventHandler, Config, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/swipe.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'swipe';\n  const EVENT_KEY = '.bs.swipe';\n  const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`;\n  const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`;\n  const EVENT_TOUCHEND = `touchend${EVENT_KEY}`;\n  const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`;\n  const EVENT_POINTERUP = `pointerup${EVENT_KEY}`;\n  const POINTER_TYPE_TOUCH = 'touch';\n  const POINTER_TYPE_PEN = 'pen';\n  const CLASS_NAME_POINTER_EVENT = 'pointer-event';\n  const SWIPE_THRESHOLD = 40;\n  const Default = {\n    endCallback: null,\n    leftCallback: null,\n    rightCallback: null\n  };\n  const DefaultType = {\n    endCallback: '(function|null)',\n    leftCallback: '(function|null)',\n    rightCallback: '(function|null)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Swipe extends Config {\n    constructor(element, config) {\n      super();\n      this._element = element;\n      if (!element || !Swipe.isSupported()) {\n        return;\n      }\n      this._config = this._getConfig(config);\n      this._deltaX = 0;\n      this._supportPointerEvents = Boolean(window.PointerEvent);\n      this._initEvents();\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    dispose() {\n      EventHandler.off(this._element, EVENT_KEY);\n    }\n\n    // Private\n    _start(event) {\n      if (!this._supportPointerEvents) {\n        this._deltaX = event.touches[0].clientX;\n        return;\n      }\n      if (this._eventIsPointerPenTouch(event)) {\n        this._deltaX = event.clientX;\n      }\n    }\n    _end(event) {\n      if (this._eventIsPointerPenTouch(event)) {\n        this._deltaX = event.clientX - this._deltaX;\n      }\n      this._handleSwipe();\n      index_js.execute(this._config.endCallback);\n    }\n    _move(event) {\n      this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX;\n    }\n    _handleSwipe() {\n      const absDeltaX = Math.abs(this._deltaX);\n      if (absDeltaX <= SWIPE_THRESHOLD) {\n        return;\n      }\n      const direction = absDeltaX / this._deltaX;\n      this._deltaX = 0;\n      if (!direction) {\n        return;\n      }\n      index_js.execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback);\n    }\n    _initEvents() {\n      if (this._supportPointerEvents) {\n        EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event));\n        EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event));\n        this._element.classList.add(CLASS_NAME_POINTER_EVENT);\n      } else {\n        EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event));\n        EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event));\n        EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event));\n      }\n    }\n    _eventIsPointerPenTouch(event) {\n      return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH);\n    }\n\n    // Static\n    static isSupported() {\n      return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;\n    }\n  }\n\n  return Swipe;\n\n}));\n//# sourceMappingURL=swipe.js.map\n", "/*!\n  * Bootstrap template-factory.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/selector-engine.js'), require('./config.js'), require('./sanitizer.js'), require('./index.js')) :\n  typeof define === 'function' && define.amd ? define(['../dom/selector-engine', './config', './sanitizer', './index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.TemplateFactory = factory(global.SelectorEngine, global.Config, global.Sanitizer, global.Index));\n})(this, (function (SelectorEngine, Config, sanitizer_js, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap util/template-factory.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'TemplateFactory';\n  const Default = {\n    allowList: sanitizer_js.DefaultAllowlist,\n    content: {},\n    // { selector : text ,  selector2 : text2 , }\n    extraClass: '',\n    html: false,\n    sanitize: true,\n    sanitizeFn: null,\n    template: '<div></div>'\n  };\n  const DefaultType = {\n    allowList: 'object',\n    content: 'object',\n    extraClass: '(string|function)',\n    html: 'boolean',\n    sanitize: 'boolean',\n    sanitizeFn: '(null|function)',\n    template: 'string'\n  };\n  const DefaultContentType = {\n    entry: '(string|element|function|null)',\n    selector: '(string|element)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class TemplateFactory extends Config {\n    constructor(config) {\n      super();\n      this._config = this._getConfig(config);\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    getContent() {\n      return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean);\n    }\n    hasContent() {\n      return this.getContent().length > 0;\n    }\n    changeContent(content) {\n      this._checkContent(content);\n      this._config.content = {\n        ...this._config.content,\n        ...content\n      };\n      return this;\n    }\n    toHtml() {\n      const templateWrapper = document.createElement('div');\n      templateWrapper.innerHTML = this._maybeSanitize(this._config.template);\n      for (const [selector, text] of Object.entries(this._config.content)) {\n        this._setContent(templateWrapper, text, selector);\n      }\n      const template = templateWrapper.children[0];\n      const extraClass = this._resolvePossibleFunction(this._config.extraClass);\n      if (extraClass) {\n        template.classList.add(...extraClass.split(' '));\n      }\n      return template;\n    }\n\n    // Private\n    _typeCheckConfig(config) {\n      super._typeCheckConfig(config);\n      this._checkContent(config.content);\n    }\n    _checkContent(arg) {\n      for (const [selector, content] of Object.entries(arg)) {\n        super._typeCheckConfig({\n          selector,\n          entry: content\n        }, DefaultContentType);\n      }\n    }\n    _setContent(template, content, selector) {\n      const templateElement = SelectorEngine.findOne(selector, template);\n      if (!templateElement) {\n        return;\n      }\n      content = this._resolvePossibleFunction(content);\n      if (!content) {\n        templateElement.remove();\n        return;\n      }\n      if (index_js.isElement(content)) {\n        this._putElementInTemplate(index_js.getElement(content), templateElement);\n        return;\n      }\n      if (this._config.html) {\n        templateElement.innerHTML = this._maybeSanitize(content);\n        return;\n      }\n      templateElement.textContent = content;\n    }\n    _maybeSanitize(arg) {\n      return this._config.sanitize ? sanitizer_js.sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;\n    }\n    _resolvePossibleFunction(arg) {\n      return index_js.execute(arg, [this]);\n    }\n    _putElementInTemplate(element, templateElement) {\n      if (this._config.html) {\n        templateElement.innerHTML = '';\n        templateElement.append(element);\n        return;\n      }\n      templateElement.textContent = element.textContent;\n    }\n  }\n\n  return TemplateFactory;\n\n}));\n//# sourceMappingURL=template-factory.js.map\n", "/*!\n  * Bootstrap base-component.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./dom/data.js'), require('./dom/event-handler.js'), require('./util/config.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./dom/data', './dom/event-handler', './util/config', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BaseComponent = factory(global.Data, global.EventHandler, global.Config, global.Index));\n})(this, (function (Data, EventHandler, Config, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap base-component.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const VERSION = '5.3.3';\n\n  /**\n   * Class definition\n   */\n\n  class BaseComponent extends Config {\n    constructor(element, config) {\n      super();\n      element = index_js.getElement(element);\n      if (!element) {\n        return;\n      }\n      this._element = element;\n      this._config = this._getConfig(config);\n      Data.set(this._element, this.constructor.DATA_KEY, this);\n    }\n\n    // Public\n    dispose() {\n      Data.remove(this._element, this.constructor.DATA_KEY);\n      EventHandler.off(this._element, this.constructor.EVENT_KEY);\n      for (const propertyName of Object.getOwnPropertyNames(this)) {\n        this[propertyName] = null;\n      }\n    }\n    _queueCallback(callback, element, isAnimated = true) {\n      index_js.executeAfterTransition(callback, element, isAnimated);\n    }\n    _getConfig(config) {\n      config = this._mergeConfigObj(config, this._element);\n      config = this._configAfterMerge(config);\n      this._typeCheckConfig(config);\n      return config;\n    }\n\n    // Static\n    static getInstance(element) {\n      return Data.get(index_js.getElement(element), this.DATA_KEY);\n    }\n    static getOrCreateInstance(element, config = {}) {\n      return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null);\n    }\n    static get VERSION() {\n      return VERSION;\n    }\n    static get DATA_KEY() {\n      return `bs.${this.NAME}`;\n    }\n    static get EVENT_KEY() {\n      return `.${this.DATA_KEY}`;\n    }\n    static eventName(name) {\n      return `${name}${this.EVENT_KEY}`;\n    }\n  }\n\n  return BaseComponent;\n\n}));\n//# sourceMappingURL=base-component.js.map\n", "/*!\n  * Bootstrap alert.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./util/component-functions.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './util/component-functions', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Alert = factory(global.BaseComponent, global.EventHandler, global.ComponentFunctions, global.Index));\n})(this, (function (BaseComponent, EventHandler, componentFunctions_js, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap alert.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'alert';\n  const DATA_KEY = 'bs.alert';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const EVENT_CLOSE = `close${EVENT_KEY}`;\n  const EVENT_CLOSED = `closed${EVENT_KEY}`;\n  const CLASS_NAME_FADE = 'fade';\n  const CLASS_NAME_SHOW = 'show';\n\n  /**\n   * Class definition\n   */\n\n  class Alert extends BaseComponent {\n    // Getters\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    close() {\n      const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE);\n      if (closeEvent.defaultPrevented) {\n        return;\n      }\n      this._element.classList.remove(CLASS_NAME_SHOW);\n      const isAnimated = this._element.classList.contains(CLASS_NAME_FADE);\n      this._queueCallback(() => this._destroyElement(), this._element, isAnimated);\n    }\n\n    // Private\n    _destroyElement() {\n      this._element.remove();\n      EventHandler.trigger(this._element, EVENT_CLOSED);\n      this.dispose();\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Alert.getOrCreateInstance(this);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config](this);\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  componentFunctions_js.enableDismissTrigger(Alert, 'close');\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Alert);\n\n  return Alert;\n\n}));\n//# sourceMappingURL=alert.js.map\n", "/*!\n  * Bootstrap button.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Button = factory(global.BaseComponent, global.EventHandler, global.Index));\n})(this, (function (BaseComponent, EventHandler, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap button.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'button';\n  const DATA_KEY = 'bs.button';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const DATA_API_KEY = '.data-api';\n  const CLASS_NAME_ACTIVE = 'active';\n  const SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"button\"]';\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;\n\n  /**\n   * Class definition\n   */\n\n  class Button extends BaseComponent {\n    // Getters\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    toggle() {\n      // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n      this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE));\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Button.getOrCreateInstance(this);\n        if (config === 'toggle') {\n          data[config]();\n        }\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {\n    event.preventDefault();\n    const button = event.target.closest(SELECTOR_DATA_TOGGLE);\n    const data = Button.getOrCreateInstance(button);\n    data.toggle();\n  });\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Button);\n\n  return Button;\n\n}));\n//# sourceMappingURL=button.js.map\n", "/*!\n  * Bootstrap carousel.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./dom/selector-engine.js'), require('./util/index.js'), require('./util/swipe.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/manipulator', './dom/selector-engine', './util/index', './util/swipe'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Carousel = factory(global.BaseComponent, global.EventHandler, global.Manipulator, global.SelectorEngine, global.Index, global.Swipe));\n})(this, (function (BaseComponent, EventHandler, Manipulator, SelectorEngine, index_js, Swipe) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap carousel.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'carousel';\n  const DATA_KEY = 'bs.carousel';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const DATA_API_KEY = '.data-api';\n  const ARROW_LEFT_KEY = 'ArrowLeft';\n  const ARROW_RIGHT_KEY = 'ArrowRight';\n  const TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch\n\n  const ORDER_NEXT = 'next';\n  const ORDER_PREV = 'prev';\n  const DIRECTION_LEFT = 'left';\n  const DIRECTION_RIGHT = 'right';\n  const EVENT_SLIDE = `slide${EVENT_KEY}`;\n  const EVENT_SLID = `slid${EVENT_KEY}`;\n  const EVENT_KEYDOWN = `keydown${EVENT_KEY}`;\n  const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`;\n  const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`;\n  const EVENT_DRAG_START = `dragstart${EVENT_KEY}`;\n  const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`;\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;\n  const CLASS_NAME_CAROUSEL = 'carousel';\n  const CLASS_NAME_ACTIVE = 'active';\n  const CLASS_NAME_SLIDE = 'slide';\n  const CLASS_NAME_END = 'carousel-item-end';\n  const CLASS_NAME_START = 'carousel-item-start';\n  const CLASS_NAME_NEXT = 'carousel-item-next';\n  const CLASS_NAME_PREV = 'carousel-item-prev';\n  const SELECTOR_ACTIVE = '.active';\n  const SELECTOR_ITEM = '.carousel-item';\n  const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM;\n  const SELECTOR_ITEM_IMG = '.carousel-item img';\n  const SELECTOR_INDICATORS = '.carousel-indicators';\n  const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]';\n  const SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]';\n  const KEY_TO_DIRECTION = {\n    [ARROW_LEFT_KEY]: DIRECTION_RIGHT,\n    [ARROW_RIGHT_KEY]: DIRECTION_LEFT\n  };\n  const Default = {\n    interval: 5000,\n    keyboard: true,\n    pause: 'hover',\n    ride: false,\n    touch: true,\n    wrap: true\n  };\n  const DefaultType = {\n    interval: '(number|boolean)',\n    // TODO:v6 remove boolean support\n    keyboard: 'boolean',\n    pause: '(string|boolean)',\n    ride: '(boolean|string)',\n    touch: 'boolean',\n    wrap: 'boolean'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Carousel extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._interval = null;\n      this._activeElement = null;\n      this._isSliding = false;\n      this.touchTimeout = null;\n      this._swipeHelper = null;\n      this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element);\n      this._addEventListeners();\n      if (this._config.ride === CLASS_NAME_CAROUSEL) {\n        this.cycle();\n      }\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    next() {\n      this._slide(ORDER_NEXT);\n    }\n    nextWhenVisible() {\n      // FIXME TODO use `document.visibilityState`\n      // Don't call next when the page isn't visible\n      // or the carousel or its parent isn't visible\n      if (!document.hidden && index_js.isVisible(this._element)) {\n        this.next();\n      }\n    }\n    prev() {\n      this._slide(ORDER_PREV);\n    }\n    pause() {\n      if (this._isSliding) {\n        index_js.triggerTransitionEnd(this._element);\n      }\n      this._clearInterval();\n    }\n    cycle() {\n      this._clearInterval();\n      this._updateInterval();\n      this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval);\n    }\n    _maybeEnableCycle() {\n      if (!this._config.ride) {\n        return;\n      }\n      if (this._isSliding) {\n        EventHandler.one(this._element, EVENT_SLID, () => this.cycle());\n        return;\n      }\n      this.cycle();\n    }\n    to(index) {\n      const items = this._getItems();\n      if (index > items.length - 1 || index < 0) {\n        return;\n      }\n      if (this._isSliding) {\n        EventHandler.one(this._element, EVENT_SLID, () => this.to(index));\n        return;\n      }\n      const activeIndex = this._getItemIndex(this._getActive());\n      if (activeIndex === index) {\n        return;\n      }\n      const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV;\n      this._slide(order, items[index]);\n    }\n    dispose() {\n      if (this._swipeHelper) {\n        this._swipeHelper.dispose();\n      }\n      super.dispose();\n    }\n\n    // Private\n    _configAfterMerge(config) {\n      config.defaultInterval = config.interval;\n      return config;\n    }\n    _addEventListeners() {\n      if (this._config.keyboard) {\n        EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event));\n      }\n      if (this._config.pause === 'hover') {\n        EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause());\n        EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle());\n      }\n      if (this._config.touch && Swipe.isSupported()) {\n        this._addTouchEventListeners();\n      }\n    }\n    _addTouchEventListeners() {\n      for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n        EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault());\n      }\n      const endCallBack = () => {\n        if (this._config.pause !== 'hover') {\n          return;\n        }\n\n        // If it's a touch-enabled device, mouseenter/leave are fired as\n        // part of the mouse compatibility events on first tap - the carousel\n        // would stop cycling until user tapped out of it;\n        // here, we listen for touchend, explicitly pause the carousel\n        // (as if it's the second time we tap on it, mouseenter compat event\n        // is NOT fired) and after a timeout (to allow for mouse compatibility\n        // events to fire) we explicitly restart cycling\n\n        this.pause();\n        if (this.touchTimeout) {\n          clearTimeout(this.touchTimeout);\n        }\n        this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval);\n      };\n      const swipeConfig = {\n        leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n        rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n        endCallback: endCallBack\n      };\n      this._swipeHelper = new Swipe(this._element, swipeConfig);\n    }\n    _keydown(event) {\n      if (/input|textarea/i.test(event.target.tagName)) {\n        return;\n      }\n      const direction = KEY_TO_DIRECTION[event.key];\n      if (direction) {\n        event.preventDefault();\n        this._slide(this._directionToOrder(direction));\n      }\n    }\n    _getItemIndex(element) {\n      return this._getItems().indexOf(element);\n    }\n    _setActiveIndicatorElement(index) {\n      if (!this._indicatorsElement) {\n        return;\n      }\n      const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement);\n      activeIndicator.classList.remove(CLASS_NAME_ACTIVE);\n      activeIndicator.removeAttribute('aria-current');\n      const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement);\n      if (newActiveIndicator) {\n        newActiveIndicator.classList.add(CLASS_NAME_ACTIVE);\n        newActiveIndicator.setAttribute('aria-current', 'true');\n      }\n    }\n    _updateInterval() {\n      const element = this._activeElement || this._getActive();\n      if (!element) {\n        return;\n      }\n      const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10);\n      this._config.interval = elementInterval || this._config.defaultInterval;\n    }\n    _slide(order, element = null) {\n      if (this._isSliding) {\n        return;\n      }\n      const activeElement = this._getActive();\n      const isNext = order === ORDER_NEXT;\n      const nextElement = element || index_js.getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap);\n      if (nextElement === activeElement) {\n        return;\n      }\n      const nextElementIndex = this._getItemIndex(nextElement);\n      const triggerEvent = eventName => {\n        return EventHandler.trigger(this._element, eventName, {\n          relatedTarget: nextElement,\n          direction: this._orderToDirection(order),\n          from: this._getItemIndex(activeElement),\n          to: nextElementIndex\n        });\n      };\n      const slideEvent = triggerEvent(EVENT_SLIDE);\n      if (slideEvent.defaultPrevented) {\n        return;\n      }\n      if (!activeElement || !nextElement) {\n        // Some weirdness is happening, so we bail\n        // TODO: change tests that use empty divs to avoid this check\n        return;\n      }\n      const isCycling = Boolean(this._interval);\n      this.pause();\n      this._isSliding = true;\n      this._setActiveIndicatorElement(nextElementIndex);\n      this._activeElement = nextElement;\n      const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END;\n      const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV;\n      nextElement.classList.add(orderClassName);\n      index_js.reflow(nextElement);\n      activeElement.classList.add(directionalClassName);\n      nextElement.classList.add(directionalClassName);\n      const completeCallBack = () => {\n        nextElement.classList.remove(directionalClassName, orderClassName);\n        nextElement.classList.add(CLASS_NAME_ACTIVE);\n        activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName);\n        this._isSliding = false;\n        triggerEvent(EVENT_SLID);\n      };\n      this._queueCallback(completeCallBack, activeElement, this._isAnimated());\n      if (isCycling) {\n        this.cycle();\n      }\n    }\n    _isAnimated() {\n      return this._element.classList.contains(CLASS_NAME_SLIDE);\n    }\n    _getActive() {\n      return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element);\n    }\n    _getItems() {\n      return SelectorEngine.find(SELECTOR_ITEM, this._element);\n    }\n    _clearInterval() {\n      if (this._interval) {\n        clearInterval(this._interval);\n        this._interval = null;\n      }\n    }\n    _directionToOrder(direction) {\n      if (index_js.isRTL()) {\n        return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT;\n      }\n      return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV;\n    }\n    _orderToDirection(order) {\n      if (index_js.isRTL()) {\n        return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT;\n      }\n      return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT;\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Carousel.getOrCreateInstance(this, config);\n        if (typeof config === 'number') {\n          data.to(config);\n          return;\n        }\n        if (typeof config === 'string') {\n          if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n            throw new TypeError(`No method named \"${config}\"`);\n          }\n          data[config]();\n        }\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) {\n    const target = SelectorEngine.getElementFromSelector(this);\n    if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n      return;\n    }\n    event.preventDefault();\n    const carousel = Carousel.getOrCreateInstance(target);\n    const slideIndex = this.getAttribute('data-bs-slide-to');\n    if (slideIndex) {\n      carousel.to(slideIndex);\n      carousel._maybeEnableCycle();\n      return;\n    }\n    if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n      carousel.next();\n      carousel._maybeEnableCycle();\n      return;\n    }\n    carousel.prev();\n    carousel._maybeEnableCycle();\n  });\n  EventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n    const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE);\n    for (const carousel of carousels) {\n      Carousel.getOrCreateInstance(carousel);\n    }\n  });\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Carousel);\n\n  return Carousel;\n\n}));\n//# sourceMappingURL=carousel.js.map\n", "/*!\n  * Bootstrap collapse.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Collapse = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index));\n})(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap collapse.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'collapse';\n  const DATA_KEY = 'bs.collapse';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const DATA_API_KEY = '.data-api';\n  const EVENT_SHOW = `show${EVENT_KEY}`;\n  const EVENT_SHOWN = `shown${EVENT_KEY}`;\n  const EVENT_HIDE = `hide${EVENT_KEY}`;\n  const EVENT_HIDDEN = `hidden${EVENT_KEY}`;\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;\n  const CLASS_NAME_SHOW = 'show';\n  const CLASS_NAME_COLLAPSE = 'collapse';\n  const CLASS_NAME_COLLAPSING = 'collapsing';\n  const CLASS_NAME_COLLAPSED = 'collapsed';\n  const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`;\n  const CLASS_NAME_HORIZONTAL = 'collapse-horizontal';\n  const WIDTH = 'width';\n  const HEIGHT = 'height';\n  const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing';\n  const SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"collapse\"]';\n  const Default = {\n    parent: null,\n    toggle: true\n  };\n  const DefaultType = {\n    parent: '(null|element)',\n    toggle: 'boolean'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Collapse extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._isTransitioning = false;\n      this._triggerArray = [];\n      const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE);\n      for (const elem of toggleList) {\n        const selector = SelectorEngine.getSelectorFromElement(elem);\n        const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element);\n        if (selector !== null && filterElement.length) {\n          this._triggerArray.push(elem);\n        }\n      }\n      this._initializeChildren();\n      if (!this._config.parent) {\n        this._addAriaAndCollapsedClass(this._triggerArray, this._isShown());\n      }\n      if (this._config.toggle) {\n        this.toggle();\n      }\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    toggle() {\n      if (this._isShown()) {\n        this.hide();\n      } else {\n        this.show();\n      }\n    }\n    show() {\n      if (this._isTransitioning || this._isShown()) {\n        return;\n      }\n      let activeChildren = [];\n\n      // find active children\n      if (this._config.parent) {\n        activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, {\n          toggle: false\n        }));\n      }\n      if (activeChildren.length && activeChildren[0]._isTransitioning) {\n        return;\n      }\n      const startEvent = EventHandler.trigger(this._element, EVENT_SHOW);\n      if (startEvent.defaultPrevented) {\n        return;\n      }\n      for (const activeInstance of activeChildren) {\n        activeInstance.hide();\n      }\n      const dimension = this._getDimension();\n      this._element.classList.remove(CLASS_NAME_COLLAPSE);\n      this._element.classList.add(CLASS_NAME_COLLAPSING);\n      this._element.style[dimension] = 0;\n      this._addAriaAndCollapsedClass(this._triggerArray, true);\n      this._isTransitioning = true;\n      const complete = () => {\n        this._isTransitioning = false;\n        this._element.classList.remove(CLASS_NAME_COLLAPSING);\n        this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW);\n        this._element.style[dimension] = '';\n        EventHandler.trigger(this._element, EVENT_SHOWN);\n      };\n      const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);\n      const scrollSize = `scroll${capitalizedDimension}`;\n      this._queueCallback(complete, this._element, true);\n      this._element.style[dimension] = `${this._element[scrollSize]}px`;\n    }\n    hide() {\n      if (this._isTransitioning || !this._isShown()) {\n        return;\n      }\n      const startEvent = EventHandler.trigger(this._element, EVENT_HIDE);\n      if (startEvent.defaultPrevented) {\n        return;\n      }\n      const dimension = this._getDimension();\n      this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`;\n      index_js.reflow(this._element);\n      this._element.classList.add(CLASS_NAME_COLLAPSING);\n      this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW);\n      for (const trigger of this._triggerArray) {\n        const element = SelectorEngine.getElementFromSelector(trigger);\n        if (element && !this._isShown(element)) {\n          this._addAriaAndCollapsedClass([trigger], false);\n        }\n      }\n      this._isTransitioning = true;\n      const complete = () => {\n        this._isTransitioning = false;\n        this._element.classList.remove(CLASS_NAME_COLLAPSING);\n        this._element.classList.add(CLASS_NAME_COLLAPSE);\n        EventHandler.trigger(this._element, EVENT_HIDDEN);\n      };\n      this._element.style[dimension] = '';\n      this._queueCallback(complete, this._element, true);\n    }\n    _isShown(element = this._element) {\n      return element.classList.contains(CLASS_NAME_SHOW);\n    }\n\n    // Private\n    _configAfterMerge(config) {\n      config.toggle = Boolean(config.toggle); // Coerce string values\n      config.parent = index_js.getElement(config.parent);\n      return config;\n    }\n    _getDimension() {\n      return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT;\n    }\n    _initializeChildren() {\n      if (!this._config.parent) {\n        return;\n      }\n      const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE);\n      for (const element of children) {\n        const selected = SelectorEngine.getElementFromSelector(element);\n        if (selected) {\n          this._addAriaAndCollapsedClass([element], this._isShown(selected));\n        }\n      }\n    }\n    _getFirstLevelChildren(selector) {\n      const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent);\n      // remove children if greater depth\n      return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element));\n    }\n    _addAriaAndCollapsedClass(triggerArray, isOpen) {\n      if (!triggerArray.length) {\n        return;\n      }\n      for (const element of triggerArray) {\n        element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen);\n        element.setAttribute('aria-expanded', isOpen);\n      }\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      const _config = {};\n      if (typeof config === 'string' && /show|hide/.test(config)) {\n        _config.toggle = false;\n      }\n      return this.each(function () {\n        const data = Collapse.getOrCreateInstance(this, _config);\n        if (typeof config === 'string') {\n          if (typeof data[config] === 'undefined') {\n            throw new TypeError(`No method named \"${config}\"`);\n          }\n          data[config]();\n        }\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n    // preventDefault only for <a> elements (which change the URL) not inside the collapsible element\n    if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') {\n      event.preventDefault();\n    }\n    for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n      Collapse.getOrCreateInstance(element, {\n        toggle: false\n      }).toggle();\n    }\n  });\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Collapse);\n\n  return Collapse;\n\n}));\n//# sourceMappingURL=collapse.js.map\n", "/*!\n  * Bootstrap dropdown.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@popperjs/core'), require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./dom/selector-engine.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['@popperjs/core', './base-component', './dom/event-handler', './dom/manipulator', './dom/selector-engine', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Dropdown = factory(global.Popper, global.BaseComponent, global.EventHandler, global.Manipulator, global.SelectorEngine, global.Index));\n})(this, (function (Popper, BaseComponent, EventHandler, Manipulator, SelectorEngine, index_js) { 'use strict';\n\n  function _interopNamespaceDefault(e) {\n    const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } });\n    if (e) {\n      for (const k in e) {\n        if (k !== 'default') {\n          const d = Object.getOwnPropertyDescriptor(e, k);\n          Object.defineProperty(n, k, d.get ? d : {\n            enumerable: true,\n            get: () => e[k]\n          });\n        }\n      }\n    }\n    n.default = e;\n    return Object.freeze(n);\n  }\n\n  const Popper__namespace = /*#__PURE__*/_interopNamespaceDefault(Popper);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap dropdown.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'dropdown';\n  const DATA_KEY = 'bs.dropdown';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const DATA_API_KEY = '.data-api';\n  const ESCAPE_KEY = 'Escape';\n  const TAB_KEY = 'Tab';\n  const ARROW_UP_KEY = 'ArrowUp';\n  const ARROW_DOWN_KEY = 'ArrowDown';\n  const RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button\n\n  const EVENT_HIDE = `hide${EVENT_KEY}`;\n  const EVENT_HIDDEN = `hidden${EVENT_KEY}`;\n  const EVENT_SHOW = `show${EVENT_KEY}`;\n  const EVENT_SHOWN = `shown${EVENT_KEY}`;\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;\n  const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`;\n  const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`;\n  const CLASS_NAME_SHOW = 'show';\n  const CLASS_NAME_DROPUP = 'dropup';\n  const CLASS_NAME_DROPEND = 'dropend';\n  const CLASS_NAME_DROPSTART = 'dropstart';\n  const CLASS_NAME_DROPUP_CENTER = 'dropup-center';\n  const CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center';\n  const SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)';\n  const SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE}.${CLASS_NAME_SHOW}`;\n  const SELECTOR_MENU = '.dropdown-menu:not(.o-dropdown--menu)'; // Odoo fix task-2764821\n  const SELECTOR_NAVBAR = '.navbar';\n  const SELECTOR_MENU_NOT_SUB = '.dropdown-menu:not(.o-dropdown--menu):not(.o_wysiwyg_submenu)';\n  const SELECTOR_NAVBAR_NAV = '.navbar-nav';\n  const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)';\n  const PLACEMENT_TOP = index_js.isRTL() ? 'top-end' : 'top-start';\n  const PLACEMENT_TOPEND = index_js.isRTL() ? 'top-start' : 'top-end';\n  const PLACEMENT_BOTTOM = index_js.isRTL() ? 'bottom-end' : 'bottom-start';\n  const PLACEMENT_BOTTOMEND = index_js.isRTL() ? 'bottom-start' : 'bottom-end';\n  const PLACEMENT_RIGHT = index_js.isRTL() ? 'left-start' : 'right-start';\n  const PLACEMENT_LEFT = index_js.isRTL() ? 'right-start' : 'left-start';\n  const PLACEMENT_TOPCENTER = 'top';\n  const PLACEMENT_BOTTOMCENTER = 'bottom';\n  const Default = {\n    autoClose: true,\n    boundary: 'clippingParents',\n    display: 'dynamic',\n    offset: [0, 2],\n    popperConfig: null,\n    reference: 'toggle'\n  };\n  const DefaultType = {\n    autoClose: '(boolean|string)',\n    boundary: '(string|element)',\n    display: 'string',\n    offset: '(array|string|function)',\n    popperConfig: '(null|object|function)',\n    reference: '(string|element|object)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Dropdown extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._popper = null;\n      this._parent = this._element.parentNode; // dropdown wrapper\n      // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n      this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent);\n      this._inNavbar = this._detectNavbar();\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    toggle() {\n      return this._isShown() ? this.hide() : this.show();\n    }\n    show() {\n      if (index_js.isDisabled(this._element) || this._isShown()) {\n        return;\n      }\n      const relatedTarget = {\n        relatedTarget: this._element\n      };\n      const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget);\n      if (showEvent.defaultPrevented) {\n        return;\n      }\n      this._createPopper();\n\n      // If this is a touch-enabled device we add extra\n      // empty mouseover listeners to the body's immediate children;\n      // only needed because of broken event delegation on iOS\n      // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n      if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n        for (const element of [].concat(...document.body.children)) {\n          EventHandler.on(element, 'mouseover', index_js.noop);\n        }\n      }\n      this._element.focus();\n      this._element.setAttribute('aria-expanded', true);\n      this._menu.classList.add(CLASS_NAME_SHOW);\n      this._element.classList.add(CLASS_NAME_SHOW);\n      EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget);\n    }\n    hide() {\n      if (index_js.isDisabled(this._element) || !this._isShown()) {\n        return;\n      }\n      const relatedTarget = {\n        relatedTarget: this._element\n      };\n      this._completeHide(relatedTarget);\n    }\n    dispose() {\n      if (this._popper) {\n        this._popper.destroy();\n      }\n      super.dispose();\n    }\n    update() {\n      this._inNavbar = this._detectNavbar();\n      if (this._popper) {\n        this._popper.update();\n      }\n    }\n\n    // Private\n    _completeHide(relatedTarget) {\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n\n      // If this is a touch-enabled device we remove the extra\n      // empty mouseover listeners we added for iOS support\n      if ('ontouchstart' in document.documentElement) {\n        for (const element of [].concat(...document.body.children)) {\n          EventHandler.off(element, 'mouseover', index_js.noop);\n        }\n      }\n      if (this._popper) {\n        this._popper.destroy();\n      }\n      this._menu.classList.remove(CLASS_NAME_SHOW);\n      this._element.classList.remove(CLASS_NAME_SHOW);\n      this._element.setAttribute('aria-expanded', 'false');\n      Manipulator.removeDataAttribute(this._menu, 'popper');\n      EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget);\n    }\n    _getConfig(config) {\n      config = super._getConfig(config);\n      if (typeof config.reference === 'object' && !index_js.isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') {\n        // Popper virtual elements require a getBoundingClientRect method\n        throw new TypeError(`${NAME.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`);\n      }\n      return config;\n    }\n    _createPopper() {\n      if (typeof Popper__namespace === 'undefined') {\n        throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org)');\n      }\n      let referenceElement = this._element;\n      if (this._config.reference === 'parent') {\n        referenceElement = this._parent;\n      } else if (index_js.isElement(this._config.reference)) {\n        referenceElement = index_js.getElement(this._config.reference);\n      } else if (typeof this._config.reference === 'object') {\n        referenceElement = this._config.reference;\n      }\n      const popperConfig = this._getPopperConfig();\n      this._popper = Popper__namespace.createPopper(referenceElement, this._menu, popperConfig);\n    }\n    _isShown() {\n      return this._menu.classList.contains(CLASS_NAME_SHOW);\n    }\n    _getPlacement() {\n      const parentDropdown = this._parent;\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n        return PLACEMENT_RIGHT;\n      }\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n        return PLACEMENT_LEFT;\n      }\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n        return PLACEMENT_TOPCENTER;\n      }\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n        return PLACEMENT_BOTTOMCENTER;\n      }\n\n      // We need to trim the value because custom properties can also include spaces\n      const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end';\n      if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n        return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP;\n      }\n      return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM;\n    }\n    _detectNavbar() {\n      return this._element.closest(SELECTOR_NAVBAR) !== null;\n    }\n    _getOffset() {\n      const {\n        offset\n      } = this._config;\n      if (typeof offset === 'string') {\n        return offset.split(',').map(value => Number.parseInt(value, 10));\n      }\n      if (typeof offset === 'function') {\n        return popperData => offset(popperData, this._element);\n      }\n      return offset;\n    }\n    _getPopperConfig() {\n      const defaultBsPopperConfig = {\n        placement: this._getPlacement(),\n        modifiers: [{\n          name: 'preventOverflow',\n          options: {\n            boundary: this._config.boundary\n          }\n        }, {\n          name: 'offset',\n          options: {\n            offset: this._getOffset()\n          }\n        }]\n      };\n\n      // Disable Popper if we have a static display or Dropdown is in Navbar\n      if (this._inNavbar || this._config.display === 'static') {\n        Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove\n        defaultBsPopperConfig.modifiers = [{\n          name: 'applyStyles',\n          enabled: false\n        }];\n      }\n      return {\n        ...defaultBsPopperConfig,\n        ...index_js.execute(this._config.popperConfig, [defaultBsPopperConfig])\n      };\n    }\n    _selectMenuItem({\n      key,\n      target\n    }) {\n      const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => index_js.isVisible(element));\n      if (!items.length) {\n        return;\n      }\n\n      // if target isn't included in items (e.g. when expanding the dropdown)\n      // allow cycling to get the last item in case key equals ARROW_UP_KEY\n      index_js.getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus();\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Dropdown.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (typeof data[config] === 'undefined') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n    static clearMenus(event) {\n      if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY) {\n        return;\n      }\n      const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN);\n      for (const toggle of openToggles) {\n        const context = Dropdown.getInstance(toggle);\n        if (!context || context._config.autoClose === false) {\n          continue;\n        }\n        const composedPath = event.composedPath();\n        const isMenuTarget = composedPath.includes(context._menu);\n        if (composedPath.includes(context._element) || context._config.autoClose === 'inside' && !isMenuTarget || context._config.autoClose === 'outside' && isMenuTarget) {\n          continue;\n        }\n\n        // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n        if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n          continue;\n        }\n        const relatedTarget = {\n          relatedTarget: context._element\n        };\n        if (event.type === 'click') {\n          relatedTarget.clickEvent = event;\n        }\n        context._completeHide(relatedTarget);\n      }\n    }\n    static dataApiKeydownHandler(event) {\n      // If not an UP | DOWN | ESCAPE key => not a dropdown command\n      // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n      const isInput = /input|textarea/i.test(event.target.tagName);\n      const isEscapeEvent = event.key === ESCAPE_KEY;\n      const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key);\n      if (!isUpOrDownEvent && !isEscapeEvent) {\n        return;\n      }\n      if (isInput && !isEscapeEvent) {\n        return;\n      }\n      event.preventDefault();\n\n      // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n      const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, event.delegateTarget.parentNode);\n      const instance = Dropdown.getOrCreateInstance(getToggleButton);\n      if (isUpOrDownEvent) {\n        event.stopPropagation();\n        instance.show();\n        instance._selectMenuItem(event);\n        return;\n      }\n      if (instance._isShown()) {\n        // else is escape and we check if it is shown\n        event.stopPropagation();\n        instance.hide();\n        getToggleButton.focus();\n      }\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler);\n  EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU_NOT_SUB, Dropdown.dataApiKeydownHandler);\n  EventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus);\n  EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus);\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n    event.preventDefault();\n    Dropdown.getOrCreateInstance(this).toggle();\n  });\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Dropdown);\n\n  return Dropdown;\n\n}));\n//# sourceMappingURL=dropdown.js.map\n", "/*!\n  * Bootstrap modal.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/backdrop.js'), require('./util/component-functions.js'), require('./util/focustrap.js'), require('./util/index.js'), require('./util/scrollbar.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/backdrop', './util/component-functions', './util/focustrap', './util/index', './util/scrollbar'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Modal = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Backdrop, global.ComponentFunctions, global.Focustrap, global.Index, global.Scrollbar));\n})(this, (function (BaseComponent, EventHandler, SelectorEngine, Backdrop, componentFunctions_js, FocusTrap, index_js, ScrollBarHelper) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap modal.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'modal';\n  const DATA_KEY = 'bs.modal';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const DATA_API_KEY = '.data-api';\n  const ESCAPE_KEY = 'Escape';\n  const EVENT_HIDE = `hide${EVENT_KEY}`;\n  const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`;\n  const EVENT_HIDDEN = `hidden${EVENT_KEY}`;\n  const EVENT_SHOW = `show${EVENT_KEY}`;\n  const EVENT_SHOWN = `shown${EVENT_KEY}`;\n  const EVENT_RESIZE = `resize${EVENT_KEY}`;\n  const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`;\n  const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`;\n  const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`;\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;\n  const CLASS_NAME_OPEN = 'modal-open';\n  const CLASS_NAME_FADE = 'fade';\n  const CLASS_NAME_SHOW = 'show';\n  const CLASS_NAME_STATIC = 'modal-static';\n  const OPEN_SELECTOR = '.modal.show';\n  const SELECTOR_DIALOG = '.modal-dialog';\n  const SELECTOR_MODAL_BODY = '.modal-body';\n  const SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"modal\"]';\n  const Default = {\n    backdrop: true,\n    focus: true,\n    keyboard: true\n  };\n  const DefaultType = {\n    backdrop: '(boolean|string)',\n    focus: 'boolean',\n    keyboard: 'boolean'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Modal extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);\n      this._backdrop = this._initializeBackDrop();\n      this._focustrap = this._initializeFocusTrap();\n      this._isShown = false;\n      this._isTransitioning = false;\n      this._scrollBar = new ScrollBarHelper();\n      this._addEventListeners();\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    toggle(relatedTarget) {\n      return this._isShown ? this.hide() : this.show(relatedTarget);\n    }\n    show(relatedTarget) {\n      if (this._isShown || this._isTransitioning) {\n        return;\n      }\n      const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {\n        relatedTarget\n      });\n      if (showEvent.defaultPrevented) {\n        return;\n      }\n      this._isShown = true;\n      this._isTransitioning = true;\n      this._scrollBar.hide();\n      document.body.classList.add(CLASS_NAME_OPEN);\n      this._adjustDialog();\n      this._backdrop.show(() => this._showElement(relatedTarget));\n    }\n    hide() {\n      if (!this._isShown || this._isTransitioning) {\n        return;\n      }\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      this._isShown = false;\n      this._isTransitioning = true;\n      this._focustrap.deactivate();\n      this._element.classList.remove(CLASS_NAME_SHOW);\n      this._queueCallback(() => this._hideModal(), this._element, this._isAnimated());\n    }\n    dispose() {\n      EventHandler.off(window, EVENT_KEY);\n      EventHandler.off(this._dialog, EVENT_KEY);\n      this._backdrop.dispose();\n      this._focustrap.deactivate();\n      super.dispose();\n    }\n    handleUpdate() {\n      this._adjustDialog();\n    }\n\n    // Private\n    _initializeBackDrop() {\n      return new Backdrop({\n        isVisible: Boolean(this._config.backdrop),\n        // 'static' option will be translated to true, and booleans will keep their value,\n        isAnimated: this._isAnimated()\n      });\n    }\n    _initializeFocusTrap() {\n      return new FocusTrap({\n        trapElement: this._element\n      });\n    }\n    _showElement(relatedTarget) {\n      // try to append dynamic modal\n      if (!document.body.contains(this._element)) {\n        document.body.append(this._element);\n      }\n      this._element.style.display = 'block';\n      this._element.removeAttribute('aria-hidden');\n      this._element.setAttribute('aria-modal', true);\n      this._element.setAttribute('role', 'dialog');\n      this._element.scrollTop = 0;\n      const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog);\n      if (modalBody) {\n        modalBody.scrollTop = 0;\n      }\n      index_js.reflow(this._element);\n      this._element.classList.add(CLASS_NAME_SHOW);\n      const transitionComplete = () => {\n        if (this._config.focus) {\n          this._focustrap.activate();\n        }\n        this._isTransitioning = false;\n        EventHandler.trigger(this._element, EVENT_SHOWN, {\n          relatedTarget\n        });\n      };\n      this._queueCallback(transitionComplete, this._dialog, this._isAnimated());\n    }\n    _addEventListeners() {\n      EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n        if (event.key !== ESCAPE_KEY) {\n          return;\n        }\n        if (this._config.keyboard) {\n          this.hide();\n          return;\n        }\n        this._triggerBackdropTransition();\n      });\n      EventHandler.on(window, EVENT_RESIZE, () => {\n        if (this._isShown && !this._isTransitioning) {\n          this._adjustDialog();\n        }\n      });\n      EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n        // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n        EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n          if (this._element !== event.target || this._element !== event2.target) {\n            return;\n          }\n          if (this._config.backdrop === 'static') {\n            this._triggerBackdropTransition();\n            return;\n          }\n          if (this._config.backdrop) {\n            this.hide();\n          }\n        });\n      });\n    }\n    _hideModal() {\n      this._element.style.display = 'none';\n      this._element.setAttribute('aria-hidden', true);\n      this._element.removeAttribute('aria-modal');\n      this._element.removeAttribute('role');\n      this._isTransitioning = false;\n      this._backdrop.hide(() => {\n        document.body.classList.remove(CLASS_NAME_OPEN);\n        this._resetAdjustments();\n        this._scrollBar.reset();\n        EventHandler.trigger(this._element, EVENT_HIDDEN);\n      });\n    }\n    _isAnimated() {\n      return this._element.classList.contains(CLASS_NAME_FADE);\n    }\n    _triggerBackdropTransition() {\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n      const initialOverflowY = this._element.style.overflowY;\n      // return if the following background transition hasn't yet completed\n      if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n        return;\n      }\n      if (!isModalOverflowing) {\n        this._element.style.overflowY = 'hidden';\n      }\n      this._element.classList.add(CLASS_NAME_STATIC);\n      this._queueCallback(() => {\n        this._element.classList.remove(CLASS_NAME_STATIC);\n        this._queueCallback(() => {\n          this._element.style.overflowY = initialOverflowY;\n        }, this._dialog);\n      }, this._dialog);\n      this._element.focus();\n    }\n\n    /**\n     * The following methods are used to handle overflowing modals\n     */\n\n    _adjustDialog() {\n      const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n      const scrollbarWidth = this._scrollBar.getWidth();\n      const isBodyOverflowing = scrollbarWidth > 0;\n      if (isBodyOverflowing && !isModalOverflowing) {\n        const property = index_js.isRTL() ? 'paddingLeft' : 'paddingRight';\n        this._element.style[property] = `${scrollbarWidth}px`;\n      }\n      if (!isBodyOverflowing && isModalOverflowing) {\n        const property = index_js.isRTL() ? 'paddingRight' : 'paddingLeft';\n        this._element.style[property] = `${scrollbarWidth}px`;\n      }\n    }\n    _resetAdjustments() {\n      this._element.style.paddingLeft = '';\n      this._element.style.paddingRight = '';\n    }\n\n    // Static\n    static jQueryInterface(config, relatedTarget) {\n      return this.each(function () {\n        const data = Modal.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (typeof data[config] === 'undefined') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config](relatedTarget);\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n    const target = SelectorEngine.getElementFromSelector(this);\n    if (['A', 'AREA'].includes(this.tagName)) {\n      event.preventDefault();\n    }\n    EventHandler.one(target, EVENT_SHOW, showEvent => {\n      if (showEvent.defaultPrevented) {\n        // only register focus restorer if modal will actually get shown\n        return;\n      }\n      EventHandler.one(target, EVENT_HIDDEN, () => {\n        if (index_js.isVisible(this)) {\n          this.focus();\n        }\n      });\n    });\n\n    // avoid conflict when clicking modal toggler while another one is open\n    const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);\n    if (alreadyOpen) {\n      Modal.getInstance(alreadyOpen).hide();\n    }\n    const data = Modal.getOrCreateInstance(target);\n    data.toggle(this);\n  });\n  componentFunctions_js.enableDismissTrigger(Modal);\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Modal);\n\n  return Modal;\n\n}));\n//# sourceMappingURL=modal.js.map\n", "/*!\n  * Bootstrap offcanvas.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/backdrop.js'), require('./util/component-functions.js'), require('./util/focustrap.js'), require('./util/index.js'), require('./util/scrollbar.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/backdrop', './util/component-functions', './util/focustrap', './util/index', './util/scrollbar'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Offcanvas = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Backdrop, global.ComponentFunctions, global.Focustrap, global.Index, global.Scrollbar));\n})(this, (function (BaseComponent, EventHandler, SelectorEngine, Backdrop, componentFunctions_js, FocusTrap, index_js, ScrollBarHelper) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap offcanvas.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'offcanvas';\n  const DATA_KEY = 'bs.offcanvas';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const DATA_API_KEY = '.data-api';\n  const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`;\n  const ESCAPE_KEY = 'Escape';\n  const CLASS_NAME_SHOW = 'show';\n  const CLASS_NAME_SHOWING = 'showing';\n  const CLASS_NAME_HIDING = 'hiding';\n  const CLASS_NAME_BACKDROP = 'offcanvas-backdrop';\n  const OPEN_SELECTOR = '.offcanvas.show';\n  const EVENT_SHOW = `show${EVENT_KEY}`;\n  const EVENT_SHOWN = `shown${EVENT_KEY}`;\n  const EVENT_HIDE = `hide${EVENT_KEY}`;\n  const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`;\n  const EVENT_HIDDEN = `hidden${EVENT_KEY}`;\n  const EVENT_RESIZE = `resize${EVENT_KEY}`;\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;\n  const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`;\n  const SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"offcanvas\"]';\n  const Default = {\n    backdrop: true,\n    keyboard: true,\n    scroll: false\n  };\n  const DefaultType = {\n    backdrop: '(boolean|string)',\n    keyboard: 'boolean',\n    scroll: 'boolean'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Offcanvas extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._isShown = false;\n      this._backdrop = this._initializeBackDrop();\n      this._focustrap = this._initializeFocusTrap();\n      this._addEventListeners();\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    toggle(relatedTarget) {\n      return this._isShown ? this.hide() : this.show(relatedTarget);\n    }\n    show(relatedTarget) {\n      if (this._isShown) {\n        return;\n      }\n      const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {\n        relatedTarget\n      });\n      if (showEvent.defaultPrevented) {\n        return;\n      }\n      this._isShown = true;\n      this._backdrop.show();\n      if (!this._config.scroll) {\n        new ScrollBarHelper().hide();\n      }\n      this._element.setAttribute('aria-modal', true);\n      this._element.setAttribute('role', 'dialog');\n      this._element.classList.add(CLASS_NAME_SHOWING);\n      const completeCallBack = () => {\n        if (!this._config.scroll || this._config.backdrop) {\n          this._focustrap.activate();\n        }\n        this._element.classList.add(CLASS_NAME_SHOW);\n        this._element.classList.remove(CLASS_NAME_SHOWING);\n        EventHandler.trigger(this._element, EVENT_SHOWN, {\n          relatedTarget\n        });\n      };\n      this._queueCallback(completeCallBack, this._element, true);\n    }\n    hide() {\n      if (!this._isShown) {\n        return;\n      }\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      this._focustrap.deactivate();\n      this._element.blur();\n      this._isShown = false;\n      this._element.classList.add(CLASS_NAME_HIDING);\n      this._backdrop.hide();\n      const completeCallback = () => {\n        this._element.classList.remove(CLASS_NAME_SHOW, CLASS_NAME_HIDING);\n        this._element.removeAttribute('aria-modal');\n        this._element.removeAttribute('role');\n        if (!this._config.scroll) {\n          new ScrollBarHelper().reset();\n        }\n        EventHandler.trigger(this._element, EVENT_HIDDEN);\n      };\n      this._queueCallback(completeCallback, this._element, true);\n    }\n    dispose() {\n      this._backdrop.dispose();\n      this._focustrap.deactivate();\n      super.dispose();\n    }\n\n    // Private\n    _initializeBackDrop() {\n      const clickCallback = () => {\n        if (this._config.backdrop === 'static') {\n          EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n          return;\n        }\n        this.hide();\n      };\n\n      // 'static' option will be translated to true, and booleans will keep their value\n      const isVisible = Boolean(this._config.backdrop);\n      return new Backdrop({\n        className: CLASS_NAME_BACKDROP,\n        isVisible,\n        isAnimated: true,\n        rootElement: this._element.parentNode,\n        clickCallback: isVisible ? clickCallback : null\n      });\n    }\n    _initializeFocusTrap() {\n      return new FocusTrap({\n        trapElement: this._element\n      });\n    }\n    _addEventListeners() {\n      EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n        if (event.key !== ESCAPE_KEY) {\n          return;\n        }\n        if (this._config.keyboard) {\n          this.hide();\n          return;\n        }\n        EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n      });\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Offcanvas.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config](this);\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n    const target = SelectorEngine.getElementFromSelector(this);\n    if (['A', 'AREA'].includes(this.tagName)) {\n      event.preventDefault();\n    }\n    if (index_js.isDisabled(this)) {\n      return;\n    }\n    EventHandler.one(target, EVENT_HIDDEN, () => {\n      // focus on trigger when it is closed\n      if (index_js.isVisible(this)) {\n        this.focus();\n      }\n    });\n\n    // avoid conflict when clicking a toggler of an offcanvas, while another is open\n    const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);\n    if (alreadyOpen && alreadyOpen !== target) {\n      Offcanvas.getInstance(alreadyOpen).hide();\n    }\n    const data = Offcanvas.getOrCreateInstance(target);\n    data.toggle(this);\n  });\n  EventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n    for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n      Offcanvas.getOrCreateInstance(selector).show();\n    }\n  });\n  EventHandler.on(window, EVENT_RESIZE, () => {\n    for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n      if (getComputedStyle(element).position !== 'fixed') {\n        Offcanvas.getOrCreateInstance(element).hide();\n      }\n    }\n  });\n  componentFunctions_js.enableDismissTrigger(Offcanvas);\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Offcanvas);\n\n  return Offcanvas;\n\n}));\n//# sourceMappingURL=offcanvas.js.map\n", "/*!\n  * Bootstrap tooltip.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@popperjs/core'), require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./util/index.js'), require('./util/sanitizer.js'), require('./util/template-factory.js')) :\n  typeof define === 'function' && define.amd ? define(['@popperjs/core', './base-component', './dom/event-handler', './dom/manipulator', './util/index', './util/sanitizer', './util/template-factory'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Tooltip = factory(global.Popper, global.BaseComponent, global.EventHandler, global.Manipulator, global.Index, global.Sanitizer, global.TemplateFactory));\n})(this, (function (Popper, BaseComponent, EventHandler, Manipulator, index_js, sanitizer_js, TemplateFactory) { 'use strict';\n\n  function _interopNamespaceDefault(e) {\n    const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } });\n    if (e) {\n      for (const k in e) {\n        if (k !== 'default') {\n          const d = Object.getOwnPropertyDescriptor(e, k);\n          Object.defineProperty(n, k, d.get ? d : {\n            enumerable: true,\n            get: () => e[k]\n          });\n        }\n      }\n    }\n    n.default = e;\n    return Object.freeze(n);\n  }\n\n  const Popper__namespace = /*#__PURE__*/_interopNamespaceDefault(Popper);\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap tooltip.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'tooltip';\n  const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']);\n  const CLASS_NAME_FADE = 'fade';\n  const CLASS_NAME_MODAL = 'modal';\n  const CLASS_NAME_SHOW = 'show';\n  const SELECTOR_TOOLTIP_INNER = '.tooltip-inner';\n  const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;\n  const EVENT_MODAL_HIDE = 'hide.bs.modal';\n  const TRIGGER_HOVER = 'hover';\n  const TRIGGER_FOCUS = 'focus';\n  const TRIGGER_CLICK = 'click';\n  const TRIGGER_MANUAL = 'manual';\n  const EVENT_HIDE = 'hide';\n  const EVENT_HIDDEN = 'hidden';\n  const EVENT_SHOW = 'show';\n  const EVENT_SHOWN = 'shown';\n  const EVENT_INSERTED = 'inserted';\n  const EVENT_CLICK = 'click';\n  const EVENT_FOCUSIN = 'focusin';\n  const EVENT_FOCUSOUT = 'focusout';\n  const EVENT_MOUSEENTER = 'mouseenter';\n  const EVENT_MOUSELEAVE = 'mouseleave';\n  const AttachmentMap = {\n    AUTO: 'auto',\n    TOP: 'top',\n    RIGHT: index_js.isRTL() ? 'left' : 'right',\n    BOTTOM: 'bottom',\n    LEFT: index_js.isRTL() ? 'right' : 'left'\n  };\n  const Default = {\n    allowList: sanitizer_js.DefaultAllowlist,\n    animation: true,\n    boundary: 'clippingParents',\n    container: false,\n    customClass: '',\n    delay: 0,\n    fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n    html: false,\n    offset: [0, 6],\n    placement: 'top',\n    popperConfig: null,\n    sanitize: true,\n    sanitizeFn: null,\n    selector: false,\n    template: '<div class=\"tooltip\" role=\"tooltip\">' + '<div class=\"tooltip-arrow\"></div>' + '<div class=\"tooltip-inner\"></div>' + '</div>',\n    title: '',\n    trigger: 'hover focus'\n  };\n  const DefaultType = {\n    allowList: 'object',\n    animation: 'boolean',\n    boundary: '(string|element)',\n    container: '(string|element|boolean)',\n    customClass: '(string|function)',\n    delay: '(number|object)',\n    fallbackPlacements: 'array',\n    html: 'boolean',\n    offset: '(array|string|function)',\n    placement: '(string|function)',\n    popperConfig: '(null|object|function)',\n    sanitize: 'boolean',\n    sanitizeFn: '(null|function)',\n    selector: '(string|boolean)',\n    template: 'string',\n    title: '(string|element|function)',\n    trigger: 'string'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Tooltip extends BaseComponent {\n    constructor(element, config) {\n      if (typeof Popper__namespace === 'undefined') {\n        throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org)');\n      }\n      super(element, config);\n\n      // Private\n      this._isEnabled = true;\n      this._timeout = 0;\n      this._isHovered = null;\n      this._activeTrigger = {};\n      this._popper = null;\n      this._templateFactory = null;\n      this._newContent = null;\n\n      // Protected\n      this.tip = null;\n      this._setListeners();\n      if (!this._config.selector) {\n        this._fixTitle();\n      }\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    enable() {\n      this._isEnabled = true;\n    }\n    disable() {\n      this._isEnabled = false;\n    }\n    toggleEnabled() {\n      this._isEnabled = !this._isEnabled;\n    }\n    toggle() {\n      if (!this._isEnabled) {\n        return;\n      }\n      this._activeTrigger.click = !this._activeTrigger.click;\n      if (this._isShown()) {\n        this._leave();\n        return;\n      }\n      this._enter();\n    }\n    dispose() {\n      clearTimeout(this._timeout);\n      EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n      if (this._element.getAttribute('data-bs-original-title')) {\n        this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'));\n      }\n      this._disposePopper();\n      super.dispose();\n    }\n    show() {\n      if (this._element.style.display === 'none') {\n        throw new Error('Please use show on visible elements');\n      }\n      if (!(this._isWithContent() && this._isEnabled)) {\n        return;\n      }\n      const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW));\n      const shadowRoot = index_js.findShadowRoot(this._element);\n      const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element);\n      if (showEvent.defaultPrevented || !isInTheDom) {\n        return;\n      }\n\n      // TODO: v6 remove this or make it optional\n      this._disposePopper();\n      const tip = this._getTipElement();\n      this._element.setAttribute('aria-describedby', tip.getAttribute('id'));\n      const {\n        container\n      } = this._config;\n      if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n        container.append(tip);\n        EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));\n      }\n      this._popper = this._createPopper(tip);\n      tip.classList.add(CLASS_NAME_SHOW);\n\n      // If this is a touch-enabled device we add extra\n      // empty mouseover listeners to the body's immediate children;\n      // only needed because of broken event delegation on iOS\n      // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n      if ('ontouchstart' in document.documentElement) {\n        for (const element of [].concat(...document.body.children)) {\n          EventHandler.on(element, 'mouseover', index_js.noop);\n        }\n      }\n      const complete = () => {\n        EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN));\n        if (this._isHovered === false) {\n          this._leave();\n        }\n        this._isHovered = false;\n      };\n      this._queueCallback(complete, this.tip, this._isAnimated());\n    }\n    hide() {\n      if (!this._isShown()) {\n        return;\n      }\n      const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE));\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      const tip = this._getTipElement();\n      tip.classList.remove(CLASS_NAME_SHOW);\n\n      // If this is a touch-enabled device we remove the extra\n      // empty mouseover listeners we added for iOS support\n      if ('ontouchstart' in document.documentElement) {\n        for (const element of [].concat(...document.body.children)) {\n          EventHandler.off(element, 'mouseover', index_js.noop);\n        }\n      }\n      this._activeTrigger[TRIGGER_CLICK] = false;\n      this._activeTrigger[TRIGGER_FOCUS] = false;\n      this._activeTrigger[TRIGGER_HOVER] = false;\n      this._isHovered = null; // it is a trick to support manual triggering\n\n      const complete = () => {\n        if (this._isWithActiveTrigger()) {\n          return;\n        }\n        if (!this._isHovered) {\n          this._disposePopper();\n        }\n        this._element.removeAttribute('aria-describedby');\n        EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN));\n      };\n      this._queueCallback(complete, this.tip, this._isAnimated());\n    }\n    update() {\n      if (this._popper) {\n        this._popper.update();\n      }\n    }\n\n    // Protected\n    _isWithContent() {\n      return Boolean(this._getTitle());\n    }\n    _getTipElement() {\n      if (!this.tip) {\n        this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());\n      }\n      return this.tip;\n    }\n    _createTipElement(content) {\n      const tip = this._getTemplateFactory(content).toHtml();\n\n      // TODO: remove this check in v6\n      if (!tip) {\n        return null;\n      }\n      tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW);\n      // TODO: v6 the following can be achieved with CSS only\n      tip.classList.add(`bs-${this.constructor.NAME}-auto`);\n      const tipId = index_js.getUID(this.constructor.NAME).toString();\n      tip.setAttribute('id', tipId);\n      if (this._isAnimated()) {\n        tip.classList.add(CLASS_NAME_FADE);\n      }\n      return tip;\n    }\n    setContent(content) {\n      this._newContent = content;\n      if (this._isShown()) {\n        this._disposePopper();\n        this.show();\n      }\n    }\n    _getTemplateFactory(content) {\n      if (this._templateFactory) {\n        this._templateFactory.changeContent(content);\n      } else {\n        this._templateFactory = new TemplateFactory({\n          ...this._config,\n          // the `content` var has to be after `this._config`\n          // to override config.content in case of popover\n          content,\n          extraClass: this._resolvePossibleFunction(this._config.customClass)\n        });\n      }\n      return this._templateFactory;\n    }\n    _getContentForTemplate() {\n      return {\n        [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n      };\n    }\n    _getTitle() {\n      return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title');\n    }\n\n    // Private\n    _initializeOnDelegatedTarget(event) {\n      return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());\n    }\n    _isAnimated() {\n      return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE);\n    }\n    _isShown() {\n      return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW);\n    }\n    _createPopper(tip) {\n      const placement = index_js.execute(this._config.placement, [this, tip, this._element]);\n      const attachment = AttachmentMap[placement.toUpperCase()];\n      return Popper__namespace.createPopper(this._element, tip, this._getPopperConfig(attachment));\n    }\n    _getOffset() {\n      const {\n        offset\n      } = this._config;\n      if (typeof offset === 'string') {\n        return offset.split(',').map(value => Number.parseInt(value, 10));\n      }\n      if (typeof offset === 'function') {\n        return popperData => offset(popperData, this._element);\n      }\n      return offset;\n    }\n    _resolvePossibleFunction(arg) {\n      return index_js.execute(arg, [this._element]);\n    }\n    _getPopperConfig(attachment) {\n      const defaultBsPopperConfig = {\n        placement: attachment,\n        modifiers: [{\n          name: 'flip',\n          options: {\n            fallbackPlacements: this._config.fallbackPlacements\n          }\n        }, {\n          name: 'offset',\n          options: {\n            offset: this._getOffset()\n          }\n        }, {\n          name: 'preventOverflow',\n          options: {\n            boundary: this._config.boundary\n          }\n        }, {\n          name: 'arrow',\n          options: {\n            element: `.${this.constructor.NAME}-arrow`\n          }\n        }, {\n          name: 'preSetPlacement',\n          enabled: true,\n          phase: 'beforeMain',\n          fn: data => {\n            // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n            // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n            this._getTipElement().setAttribute('data-popper-placement', data.state.placement);\n          }\n        }]\n      };\n      return {\n        ...defaultBsPopperConfig,\n        ...index_js.execute(this._config.popperConfig, [defaultBsPopperConfig])\n      };\n    }\n    _setListeners() {\n      const triggers = this._config.trigger.split(' ');\n      for (const trigger of triggers) {\n        if (trigger === 'click') {\n          EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => {\n            const context = this._initializeOnDelegatedTarget(event);\n            context.toggle();\n          });\n        } else if (trigger !== TRIGGER_MANUAL) {\n          const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN);\n          const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT);\n          EventHandler.on(this._element, eventIn, this._config.selector, event => {\n            const context = this._initializeOnDelegatedTarget(event);\n            context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true;\n            context._enter();\n          });\n          EventHandler.on(this._element, eventOut, this._config.selector, event => {\n            const context = this._initializeOnDelegatedTarget(event);\n            context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget);\n            context._leave();\n          });\n        }\n      }\n      this._hideModalHandler = () => {\n        if (this._element) {\n          this.hide();\n        }\n      };\n      EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n    }\n    _fixTitle() {\n      const title = this._element.getAttribute('title');\n      if (!title) {\n        return;\n      }\n      if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n        this._element.setAttribute('aria-label', title);\n      }\n      this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility\n      this._element.removeAttribute('title');\n    }\n    _enter() {\n      if (this._isShown() || this._isHovered) {\n        this._isHovered = true;\n        return;\n      }\n      this._isHovered = true;\n      this._setTimeout(() => {\n        if (this._isHovered) {\n          this.show();\n        }\n      }, this._config.delay.show);\n    }\n    _leave() {\n      if (this._isWithActiveTrigger()) {\n        return;\n      }\n      this._isHovered = false;\n      this._setTimeout(() => {\n        if (!this._isHovered) {\n          this.hide();\n        }\n      }, this._config.delay.hide);\n    }\n    _setTimeout(handler, timeout) {\n      clearTimeout(this._timeout);\n      this._timeout = setTimeout(handler, timeout);\n    }\n    _isWithActiveTrigger() {\n      return Object.values(this._activeTrigger).includes(true);\n    }\n    _getConfig(config) {\n      const dataAttributes = Manipulator.getDataAttributes(this._element);\n      for (const dataAttribute of Object.keys(dataAttributes)) {\n        if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n          delete dataAttributes[dataAttribute];\n        }\n      }\n      config = {\n        ...dataAttributes,\n        ...(typeof config === 'object' && config ? config : {})\n      };\n      config = this._mergeConfigObj(config);\n      config = this._configAfterMerge(config);\n      this._typeCheckConfig(config);\n      return config;\n    }\n    _configAfterMerge(config) {\n      config.container = config.container === false ? document.body : index_js.getElement(config.container);\n      if (typeof config.delay === 'number') {\n        config.delay = {\n          show: config.delay,\n          hide: config.delay\n        };\n      }\n      if (typeof config.title === 'number') {\n        config.title = config.title.toString();\n      }\n      if (typeof config.content === 'number') {\n        config.content = config.content.toString();\n      }\n      return config;\n    }\n    _getDelegateConfig() {\n      const config = {};\n      for (const [key, value] of Object.entries(this._config)) {\n        if (this.constructor.Default[key] !== value) {\n          config[key] = value;\n        }\n      }\n      config.selector = false;\n      config.trigger = 'manual';\n\n      // In the future can be replaced with:\n      // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n      // `Object.fromEntries(keysWithDifferentValues)`\n      return config;\n    }\n    _disposePopper() {\n      if (this._popper) {\n        this._popper.destroy();\n        this._popper = null;\n      }\n      if (this.tip) {\n        this.tip.remove();\n        this.tip = null;\n      }\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Tooltip.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (typeof data[config] === 'undefined') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n  }\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Tooltip);\n\n  return Tooltip;\n\n}));\n//# sourceMappingURL=tooltip.js.map\n", "/*!\n  * Bootstrap popover.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./tooltip.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./tooltip', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Popover = factory(global.Tooltip, global.Index));\n})(this, (function (Tooltip, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap popover.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'popover';\n  const SELECTOR_TITLE = '.popover-header';\n  const SELECTOR_CONTENT = '.popover-body';\n  const Default = {\n    ...Tooltip.Default,\n    content: '',\n    offset: [0, 8],\n    placement: 'right',\n    template: '<div class=\"popover\" role=\"tooltip\">' + '<div class=\"popover-arrow\"></div>' + '<h3 class=\"popover-header\"></h3>' + '<div class=\"popover-body\"></div>' + '</div>',\n    trigger: 'click'\n  };\n  const DefaultType = {\n    ...Tooltip.DefaultType,\n    content: '(null|string|element|function)'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Popover extends Tooltip {\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Overrides\n    _isWithContent() {\n      return this._getTitle() || this._getContent();\n    }\n\n    // Private\n    _getContentForTemplate() {\n      return {\n        [SELECTOR_TITLE]: this._getTitle(),\n        [SELECTOR_CONTENT]: this._getContent()\n      };\n    }\n    _getContent() {\n      return this._resolvePossibleFunction(this._config.content);\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Popover.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (typeof data[config] === 'undefined') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n  }\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Popover);\n\n  return Popover;\n\n}));\n//# sourceMappingURL=popover.js.map\n", "/*!\n  * Bootstrap scrollspy.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ScrollSpy = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index));\n})(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap scrollspy.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'scrollspy';\n  const DATA_KEY = 'bs.scrollspy';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const DATA_API_KEY = '.data-api';\n  const EVENT_ACTIVATE = `activate${EVENT_KEY}`;\n  const EVENT_CLICK = `click${EVENT_KEY}`;\n  const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`;\n  const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';\n  const CLASS_NAME_ACTIVE = 'active';\n  const SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]';\n  const SELECTOR_TARGET_LINKS = '[href]';\n  const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';\n  const SELECTOR_NAV_LINKS = '.nav-link';\n  const SELECTOR_NAV_ITEMS = '.nav-item';\n  const SELECTOR_LIST_ITEMS = '.list-group-item';\n  const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;\n  const SELECTOR_DROPDOWN = '.dropdown';\n  const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle';\n  const Default = {\n    offset: null,\n    // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n    rootMargin: '0px 0px -25%',\n    smoothScroll: false,\n    target: null,\n    threshold: [0.1, 0.5, 1]\n  };\n  const DefaultType = {\n    offset: '(number|null)',\n    // TODO v6 @deprecated, keep it for backwards compatibility reasons\n    rootMargin: 'string',\n    smoothScroll: 'boolean',\n    target: 'element',\n    threshold: 'array'\n  };\n\n  /**\n   * Class definition\n   */\n\n  class ScrollSpy extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n\n      // this._element is the observablesContainer and config.target the menu links wrapper\n      this._targetLinks = new Map();\n      this._observableSections = new Map();\n      this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;\n      this._activeTarget = null;\n      this._observer = null;\n      this._previousScrollData = {\n        visibleEntryTop: 0,\n        parentScrollTop: 0\n      };\n      this.refresh(); // initialize\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    refresh() {\n      this._initializeTargetsAndObservables();\n      this._maybeEnableSmoothScroll();\n      if (this._observer) {\n        this._observer.disconnect();\n      } else {\n        this._observer = this._getNewObserver();\n      }\n      for (const section of this._observableSections.values()) {\n        this._observer.observe(section);\n      }\n    }\n    dispose() {\n      this._observer.disconnect();\n      super.dispose();\n    }\n\n    // Private\n    _configAfterMerge(config) {\n      // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n      config.target = index_js.getElement(config.target) || document.body;\n\n      // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n      config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin;\n      if (typeof config.threshold === 'string') {\n        config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value));\n      }\n      return config;\n    }\n    _maybeEnableSmoothScroll() {\n      if (!this._config.smoothScroll) {\n        return;\n      }\n\n      // unregister any previous listeners\n      EventHandler.off(this._config.target, EVENT_CLICK);\n      EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n        const observableSection = this._observableSections.get(event.target.hash);\n        if (observableSection) {\n          event.preventDefault();\n          const root = this._rootElement || window;\n          const height = observableSection.offsetTop - this._element.offsetTop;\n          if (root.scrollTo) {\n            root.scrollTo({\n              top: height,\n              behavior: 'smooth'\n            });\n            return;\n          }\n\n          // Chrome 60 doesn't support `scrollTo`\n          root.scrollTop = height;\n        }\n      });\n    }\n    _getNewObserver() {\n      const options = {\n        root: this._rootElement,\n        threshold: this._config.threshold,\n        rootMargin: this._config.rootMargin\n      };\n      return new IntersectionObserver(entries => this._observerCallback(entries), options);\n    }\n\n    // The logic of selection\n    _observerCallback(entries) {\n      const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`);\n      const activate = entry => {\n        this._previousScrollData.visibleEntryTop = entry.target.offsetTop;\n        this._process(targetElement(entry));\n      };\n      const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;\n      const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;\n      this._previousScrollData.parentScrollTop = parentScrollTop;\n      for (const entry of entries) {\n        if (!entry.isIntersecting) {\n          this._activeTarget = null;\n          this._clearActiveClass(targetElement(entry));\n          continue;\n        }\n        const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop;\n        // if we are scrolling down, pick the bigger offsetTop\n        if (userScrollsDown && entryIsLowerThanPrevious) {\n          activate(entry);\n          // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n          if (!parentScrollTop) {\n            return;\n          }\n          continue;\n        }\n\n        // if we are scrolling up, pick the smallest offsetTop\n        if (!userScrollsDown && !entryIsLowerThanPrevious) {\n          activate(entry);\n        }\n      }\n    }\n    _initializeTargetsAndObservables() {\n      this._targetLinks = new Map();\n      this._observableSections = new Map();\n      const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target);\n      for (const anchor of targetLinks) {\n        // ensure that the anchor has an id and is not disabled\n        if (!anchor.hash || index_js.isDisabled(anchor)) {\n          continue;\n        }\n        const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element);\n\n        // ensure that the observableSection exists & is visible\n        if (index_js.isVisible(observableSection)) {\n          this._targetLinks.set(decodeURI(anchor.hash), anchor);\n          this._observableSections.set(anchor.hash, observableSection);\n        }\n      }\n    }\n    _process(target) {\n      if (this._activeTarget === target) {\n        return;\n      }\n      this._clearActiveClass(this._config.target);\n      this._activeTarget = target;\n      target.classList.add(CLASS_NAME_ACTIVE);\n      this._activateParents(target);\n      EventHandler.trigger(this._element, EVENT_ACTIVATE, {\n        relatedTarget: target\n      });\n    }\n    _activateParents(target) {\n      // Activate dropdown parents\n      if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n        SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE);\n        return;\n      }\n      for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n        // Set triggered links parents as active\n        // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor\n        for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) {\n          item.classList.add(CLASS_NAME_ACTIVE);\n        }\n      }\n    }\n    _clearActiveClass(parent) {\n      parent.classList.remove(CLASS_NAME_ACTIVE);\n      const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`, parent);\n      for (const node of activeNodes) {\n        node.classList.remove(CLASS_NAME_ACTIVE);\n      }\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = ScrollSpy.getOrCreateInstance(this, config);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n    for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) {\n      ScrollSpy.getOrCreateInstance(spy);\n    }\n  });\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(ScrollSpy);\n\n  return ScrollSpy;\n\n}));\n//# sourceMappingURL=scrollspy.js.map\n", "/*!\n  * Bootstrap tab.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Tab = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index));\n})(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap tab.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'tab';\n  const DATA_KEY = 'bs.tab';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const EVENT_HIDE = `hide${EVENT_KEY}`;\n  const EVENT_HIDDEN = `hidden${EVENT_KEY}`;\n  const EVENT_SHOW = `show${EVENT_KEY}`;\n  const EVENT_SHOWN = `shown${EVENT_KEY}`;\n  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}`;\n  const EVENT_KEYDOWN = `keydown${EVENT_KEY}`;\n  const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`;\n  const ARROW_LEFT_KEY = 'ArrowLeft';\n  const ARROW_RIGHT_KEY = 'ArrowRight';\n  const ARROW_UP_KEY = 'ArrowUp';\n  const ARROW_DOWN_KEY = 'ArrowDown';\n  const HOME_KEY = 'Home';\n  const END_KEY = 'End';\n  const CLASS_NAME_ACTIVE = 'active';\n  const CLASS_NAME_FADE = 'fade';\n  const CLASS_NAME_SHOW = 'show';\n  const CLASS_DROPDOWN = 'dropdown';\n  const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle';\n  const SELECTOR_DROPDOWN_MENU = '.dropdown-menu';\n  const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`;\n  const SELECTOR_TAB_PANEL = '.list-group, .nav, [role=\"tablist\"]';\n  const SELECTOR_OUTER = '.nav-item, .list-group-item';\n  const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role=\"tab\"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`;\n  const SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"tab\"], [data-bs-toggle=\"pill\"], [data-bs-toggle=\"list\"]'; // TODO: could only be `tab` in v6\n  const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`;\n  const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle=\"tab\"], .${CLASS_NAME_ACTIVE}[data-bs-toggle=\"pill\"], .${CLASS_NAME_ACTIVE}[data-bs-toggle=\"list\"]`;\n\n  /**\n   * Class definition\n   */\n\n  class Tab extends BaseComponent {\n    constructor(element) {\n      super(element);\n      this._parent = this._element.closest(SELECTOR_TAB_PANEL);\n      if (!this._parent) {\n        return;\n        // TODO: should throw exception in v6\n        // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_INNER_ELEM}`)\n      }\n\n      // Set up initial aria attributes\n      this._setInitialAttributes(this._parent, this._getChildren());\n      EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event));\n    }\n\n    // Getters\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    show() {\n      // Shows this elem and deactivate the active sibling if exists\n      const innerElem = this._element;\n      if (this._elemIsActive(innerElem)) {\n        return;\n      }\n\n      // Search for active tab on same parent to deactivate it\n      const active = this._getActiveElem();\n      const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE, {\n        relatedTarget: innerElem\n      }) : null;\n      const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, {\n        relatedTarget: active\n      });\n      if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) {\n        return;\n      }\n      this._deactivate(active, innerElem);\n      this._activate(innerElem, active);\n    }\n\n    // Private\n    _activate(element, relatedElem) {\n      if (!element) {\n        return;\n      }\n      element.classList.add(CLASS_NAME_ACTIVE);\n      this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section\n\n      const complete = () => {\n        if (element.getAttribute('role') !== 'tab') {\n          element.classList.add(CLASS_NAME_SHOW);\n          return;\n        }\n        element.removeAttribute('tabindex');\n        element.setAttribute('aria-selected', true);\n        this._toggleDropDown(element, true);\n        EventHandler.trigger(element, EVENT_SHOWN, {\n          relatedTarget: relatedElem\n        });\n      };\n      this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE));\n    }\n    _deactivate(element, relatedElem) {\n      if (!element) {\n        return;\n      }\n      element.classList.remove(CLASS_NAME_ACTIVE);\n      element.blur();\n      this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too\n\n      const complete = () => {\n        if (element.getAttribute('role') !== 'tab') {\n          element.classList.remove(CLASS_NAME_SHOW);\n          return;\n        }\n        element.setAttribute('aria-selected', false);\n        element.setAttribute('tabindex', '-1');\n        this._toggleDropDown(element, false);\n        EventHandler.trigger(element, EVENT_HIDDEN, {\n          relatedTarget: relatedElem\n        });\n      };\n      this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE));\n    }\n    _keydown(event) {\n      if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) {\n        return;\n      }\n      event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page\n      event.preventDefault();\n      const children = this._getChildren().filter(element => !index_js.isDisabled(element));\n      let nextActiveElement;\n      if ([HOME_KEY, END_KEY].includes(event.key)) {\n        nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1];\n      } else {\n        const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key);\n        nextActiveElement = index_js.getNextActiveElement(children, event.target, isNext, true);\n      }\n      if (nextActiveElement) {\n        nextActiveElement.focus({\n          preventScroll: true\n        });\n        Tab.getOrCreateInstance(nextActiveElement).show();\n      }\n    }\n    _getChildren() {\n      // collection of inner elements\n      return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent);\n    }\n    _getActiveElem() {\n      return this._getChildren().find(child => this._elemIsActive(child)) || null;\n    }\n    _setInitialAttributes(parent, children) {\n      this._setAttributeIfNotExists(parent, 'role', 'tablist');\n      for (const child of children) {\n        this._setInitialAttributesOnChild(child);\n      }\n    }\n    _setInitialAttributesOnChild(child) {\n      child = this._getInnerElement(child);\n      const isActive = this._elemIsActive(child);\n      const outerElem = this._getOuterElement(child);\n      child.setAttribute('aria-selected', isActive);\n      if (outerElem !== child) {\n        this._setAttributeIfNotExists(outerElem, 'role', 'presentation');\n      }\n      if (!isActive) {\n        child.setAttribute('tabindex', '-1');\n      }\n      this._setAttributeIfNotExists(child, 'role', 'tab');\n\n      // set attributes to the related panel too\n      this._setInitialAttributesOnTargetPanel(child);\n    }\n    _setInitialAttributesOnTargetPanel(child) {\n      const target = SelectorEngine.getElementFromSelector(child);\n      if (!target) {\n        return;\n      }\n      this._setAttributeIfNotExists(target, 'role', 'tabpanel');\n      if (child.id) {\n        this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`);\n      }\n    }\n    _toggleDropDown(element, open) {\n      const outerElem = this._getOuterElement(element);\n      if (!outerElem.classList.contains(CLASS_DROPDOWN)) {\n        return;\n      }\n      const toggle = (selector, className) => {\n        const element = SelectorEngine.findOne(selector, outerElem);\n        if (element) {\n          element.classList.toggle(className, open);\n        }\n      };\n      toggle(SELECTOR_DROPDOWN_TOGGLE, CLASS_NAME_ACTIVE);\n      toggle(SELECTOR_DROPDOWN_MENU, CLASS_NAME_SHOW);\n      outerElem.setAttribute('aria-expanded', open);\n    }\n    _setAttributeIfNotExists(element, attribute, value) {\n      if (!element.hasAttribute(attribute)) {\n        element.setAttribute(attribute, value);\n      }\n    }\n    _elemIsActive(elem) {\n      return elem.classList.contains(CLASS_NAME_ACTIVE);\n    }\n\n    // Try to get the inner element (usually the .nav-link)\n    _getInnerElement(elem) {\n      return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem);\n    }\n\n    // Try to get the outer element (usually the .nav-item)\n    _getOuterElement(elem) {\n      return elem.closest(SELECTOR_OUTER) || elem;\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Tab.getOrCreateInstance(this);\n        if (typeof config !== 'string') {\n          return;\n        }\n        if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n          throw new TypeError(`No method named \"${config}\"`);\n        }\n        data[config]();\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n    if (['A', 'AREA'].includes(this.tagName)) {\n      event.preventDefault();\n    }\n    if (index_js.isDisabled(this)) {\n      return;\n    }\n    Tab.getOrCreateInstance(this).show();\n  });\n\n  /**\n   * Initialize on focus\n   */\n  EventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n    for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) {\n      Tab.getOrCreateInstance(element);\n    }\n  });\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Tab);\n\n  return Tab;\n\n}));\n//# sourceMappingURL=tab.js.map\n", "/*!\n  * Bootstrap toast.js v5.3.3 (https://getbootstrap.com/)\n  * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n  */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./util/component-functions.js'), require('./util/index.js')) :\n  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './util/component-functions', './util/index'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Toast = factory(global.BaseComponent, global.EventHandler, global.ComponentFunctions, global.Index));\n})(this, (function (BaseComponent, EventHandler, componentFunctions_js, index_js) { 'use strict';\n\n  /**\n   * --------------------------------------------------------------------------\n   * Bootstrap toast.js\n   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n   * --------------------------------------------------------------------------\n   */\n\n\n  /**\n   * Constants\n   */\n\n  const NAME = 'toast';\n  const DATA_KEY = 'bs.toast';\n  const EVENT_KEY = `.${DATA_KEY}`;\n  const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}`;\n  const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}`;\n  const EVENT_FOCUSIN = `focusin${EVENT_KEY}`;\n  const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`;\n  const EVENT_HIDE = `hide${EVENT_KEY}`;\n  const EVENT_HIDDEN = `hidden${EVENT_KEY}`;\n  const EVENT_SHOW = `show${EVENT_KEY}`;\n  const EVENT_SHOWN = `shown${EVENT_KEY}`;\n  const CLASS_NAME_FADE = 'fade';\n  const CLASS_NAME_HIDE = 'hide'; // @deprecated - kept here only for backwards compatibility\n  const CLASS_NAME_SHOW = 'show';\n  const CLASS_NAME_SHOWING = 'showing';\n  const DefaultType = {\n    animation: 'boolean',\n    autohide: 'boolean',\n    delay: 'number'\n  };\n  const Default = {\n    animation: true,\n    autohide: true,\n    delay: 5000\n  };\n\n  /**\n   * Class definition\n   */\n\n  class Toast extends BaseComponent {\n    constructor(element, config) {\n      super(element, config);\n      this._timeout = null;\n      this._hasMouseInteraction = false;\n      this._hasKeyboardInteraction = false;\n      this._setListeners();\n    }\n\n    // Getters\n    static get Default() {\n      return Default;\n    }\n    static get DefaultType() {\n      return DefaultType;\n    }\n    static get NAME() {\n      return NAME;\n    }\n\n    // Public\n    show() {\n      const showEvent = EventHandler.trigger(this._element, EVENT_SHOW);\n      if (showEvent.defaultPrevented) {\n        return;\n      }\n      this._clearTimeout();\n      if (this._config.animation) {\n        this._element.classList.add(CLASS_NAME_FADE);\n      }\n      const complete = () => {\n        this._element.classList.remove(CLASS_NAME_SHOWING);\n        EventHandler.trigger(this._element, EVENT_SHOWN);\n        this._maybeScheduleHide();\n      };\n      this._element.classList.remove(CLASS_NAME_HIDE); // @deprecated\n      index_js.reflow(this._element);\n      this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING);\n      this._queueCallback(complete, this._element, this._config.animation);\n    }\n    hide() {\n      if (!this.isShown()) {\n        return;\n      }\n      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE);\n      if (hideEvent.defaultPrevented) {\n        return;\n      }\n      const complete = () => {\n        this._element.classList.add(CLASS_NAME_HIDE); // @deprecated\n        this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW);\n        EventHandler.trigger(this._element, EVENT_HIDDEN);\n      };\n      this._element.classList.add(CLASS_NAME_SHOWING);\n      this._queueCallback(complete, this._element, this._config.animation);\n    }\n    dispose() {\n      this._clearTimeout();\n      if (this.isShown()) {\n        this._element.classList.remove(CLASS_NAME_SHOW);\n      }\n      super.dispose();\n    }\n    isShown() {\n      return this._element.classList.contains(CLASS_NAME_SHOW);\n    }\n\n    // Private\n\n    _maybeScheduleHide() {\n      if (!this._config.autohide) {\n        return;\n      }\n      if (this._hasMouseInteraction || this._hasKeyboardInteraction) {\n        return;\n      }\n      this._timeout = setTimeout(() => {\n        this.hide();\n      }, this._config.delay);\n    }\n    _onInteraction(event, isInteracting) {\n      switch (event.type) {\n        case 'mouseover':\n        case 'mouseout':\n          {\n            this._hasMouseInteraction = isInteracting;\n            break;\n          }\n        case 'focusin':\n        case 'focusout':\n          {\n            this._hasKeyboardInteraction = isInteracting;\n            break;\n          }\n      }\n      if (isInteracting) {\n        this._clearTimeout();\n        return;\n      }\n      const nextElement = event.relatedTarget;\n      if (this._element === nextElement || this._element.contains(nextElement)) {\n        return;\n      }\n      this._maybeScheduleHide();\n    }\n    _setListeners() {\n      EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true));\n      EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false));\n      EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true));\n      EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false));\n    }\n    _clearTimeout() {\n      clearTimeout(this._timeout);\n      this._timeout = null;\n    }\n\n    // Static\n    static jQueryInterface(config) {\n      return this.each(function () {\n        const data = Toast.getOrCreateInstance(this, config);\n        if (typeof config === 'string') {\n          if (typeof data[config] === 'undefined') {\n            throw new TypeError(`No method named \"${config}\"`);\n          }\n          data[config](this);\n        }\n      });\n    }\n  }\n\n  /**\n   * Data API implementation\n   */\n\n  componentFunctions_js.enableDismissTrigger(Toast);\n\n  /**\n   * jQuery\n   */\n\n  index_js.defineJQueryPlugin(Toast);\n\n  return Toast;\n\n}));\n//# sourceMappingURL=toast.js.map\n", "/** @odoo-module **/\n\nimport { compensateScrollbar, getScrollingElement } from \"@web/core/utils/scrolling\";\n\n/**\n * The bootstrap library extensions and fixes should be done here to avoid\n * patching in place.\n */\n\n/**\n * Review Bootstrap Sanitization: leave it enabled by default but extend it to\n * accept more common tag names like tables and buttons, and common attributes\n * such as style or data-. If a specific tooltip or popover must accept custom\n * tags or attributes, they must be supplied through the whitelist BS\n * parameter explicitely.\n *\n * We cannot disable sanitization because bootstrap uses tooltip/popover\n * DOM attributes in an \"unsafe\" way.\n */\nconst bsSanitizeAllowList = Tooltip.Default.allowList;\n\nbsSanitizeAllowList[\"*\"].push(\"title\", \"style\", /^data-[\\w-]+/);\n\nbsSanitizeAllowList.header = [];\nbsSanitizeAllowList.main = [];\nbsSanitizeAllowList.footer = [];\n\nbsSanitizeAllowList.caption = [];\nbsSanitizeAllowList.col = [\"span\"];\nbsSanitizeAllowList.colgroup = [\"span\"];\nbsSanitizeAllowList.table = [];\nbsSanitizeAllowList.thead = [];\nbsSanitizeAllowList.tbody = [];\nbsSanitizeAllowList.tfooter = [];\nbsSanitizeAllowList.tr = [];\nbsSanitizeAllowList.th = [\"colspan\", \"rowspan\"];\nbsSanitizeAllowList.td = [\"colspan\", \"rowspan\"];\n\nbsSanitizeAllowList.address = [];\nbsSanitizeAllowList.article = [];\nbsSanitizeAllowList.aside = [];\nbsSanitizeAllowList.blockquote = [];\nbsSanitizeAllowList.section = [];\n\nbsSanitizeAllowList.button = [\"type\"];\nbsSanitizeAllowList.del = [];\n\n/* Bootstrap tooltip defaults overwrite */\nTooltip.Default.placement = \"auto\";\nTooltip.Default.fallbackPlacement = [\"bottom\", \"right\", \"left\", \"top\"];\nTooltip.Default.html = true;\nTooltip.Default.trigger = \"hover\";\nTooltip.Default.container = \"body\";\nTooltip.Default.boundary = \"window\";\nTooltip.Default.delay = { show: 1000, hide: 0 };\n\nconst bootstrapShowFunction = Tooltip.prototype.show;\nTooltip.prototype.show = function () {\n    // Overwrite bootstrap tooltip method to prevent showing 2 tooltip at the\n    // same time\n    document.querySelectorAll(\".tooltip\").forEach((el) => el.remove());\n    const errorsToIgnore = [\"Please use show on visible elements\"];\n    try {\n        return bootstrapShowFunction.call(this);\n    } catch (error) {\n        if (errorsToIgnore.includes(error.message)) {\n            return 0;\n        }\n        throw error;\n    }\n};\n\n/**\n * Bootstrap disables dynamic dropdown positioning when it is in a navbar. Here\n * we make this patch to activate this dynamic navbar's dropdown positioning\n * which is useful to avoid that the elements of the website sub-menus overflow\n * the page.\n */\nDropdown.prototype._detectNavbar = function () {\n    return false;\n};\n\n/* Bootstrap modal scrollbar compensation on non-body */\nconst bsAdjustDialogFunction = Modal.prototype._adjustDialog;\nModal.prototype._adjustDialog = function () {\n    const document = this._element.ownerDocument;\n\n    this._scrollBar.reset();\n    document.body.classList.remove(\"modal-open\");\n\n    const scrollable = getScrollingElement(document);\n    if (document.body.contains(scrollable)) {\n        compensateScrollbar(scrollable, true);\n    }\n\n    this._scrollBar.hide();\n    document.body.classList.add(\"modal-open\");\n\n    return bsAdjustDialogFunction.apply(this, arguments);\n};\n\nconst bsResetAdjustmentsFunction = Modal.prototype._resetAdjustments;\nModal.prototype._resetAdjustments = function () {\n    const document = this._element.ownerDocument;\n\n    this._scrollBar.reset();\n    document.body.classList.remove(\"modal-open\");\n\n    const scrollable = getScrollingElement(document);\n    if (document.body.contains(scrollable)) {\n        compensateScrollbar(scrollable, false);\n    }\n    return bsResetAdjustmentsFunction.apply(this, arguments);\n};\n", "/** @odoo-module **/\n\n/**\n * The jquery library extensions and fixes should be done here to avoid patching\n * in place.\n */\n\n// jQuery selectors extensions\n$.extend($.expr[':'], {\n    data: function (element, index, matches) {\n        return $(element).data(matches[3]);\n    },\n});\n\n// jQuery functions extensions\n$.fn.extend({\n    /**\n     * Makes DOM elements bounce the way Odoo decided it.\n     *\n     * @param {string} [extraClass]\n     */\n    odooBounce: function (extraClass) {\n        for (const el of this) {\n            el.classList.add('o_catch_attention', extraClass);\n            setTimeout(() => el.classList.remove('o_catch_attention', extraClass), 400);\n        }\n        return this;\n    },\n    /**\n     * Allows to bind events to a handler just as the standard `$.on` function\n     * but binds the handler so that it is executed before any already-attached\n     * handler for the same events.\n     *\n     * @see jQuery.on\n     */\n    prependEvent: function (events, selector, data, handler) {\n        this.on.apply(this, arguments);\n\n        events = events.split(' ');\n        return this.each(function () {\n            var el = this;\n            events.forEach((evNameNamespaced) => {\n                var evName = evNameNamespaced.split('.')[0];\n                var handler = $._data(el, 'events')[evName].pop();\n                $._data(el, 'events')[evName].unshift(handler);\n            });\n        });\n    },\n    /**\n     * @deprecated this will soon be removed: just rely on the fact that the\n     * scrollbar is at its natural position.\n     * @returns {jQuery}\n     */\n    getScrollingElement(document = window.document) {\n        const $baseScrollingElement = $(document.scrollingElement);\n        if ($baseScrollingElement.isScrollable()\n                && $baseScrollingElement.hasScrollableContent()) {\n            return $baseScrollingElement;\n        }\n        const bodyHeight = $(document.body).height();\n        for (const el of document.body.children) {\n            // Search for a body child which is at least as tall as the body\n            // and which has the ability to scroll if enough content in it. If\n            // found, suppose this is the top scrolling element.\n            if (bodyHeight - el.scrollHeight > 1.5) {\n                continue;\n            }\n            const $el = $(el);\n            if ($el.isScrollable()) {\n                return $el;\n            }\n        }\n        return $baseScrollingElement;\n    },\n    /**\n     * @deprecated this will soon be removed: just rely on the fact that the\n     * scrollbar is at its natural position.\n     * @returns {jQuery}\n     */\n    getScrollingTarget(contextItem = window.document) {\n        // Cannot use `instanceof` because of cross-frame issues.\n        const isElement = obj => obj && obj.nodeType === Node.ELEMENT_NODE;\n        const isJQuery = obj => obj && ('jquery' in obj);\n\n        const $scrollingElement = isElement(contextItem)\n            ? $(contextItem)\n            : isJQuery(contextItem)\n            ? contextItem\n            : $().getScrollingElement(contextItem);\n        const document = $scrollingElement[0].ownerDocument;\n        return $scrollingElement.is(document.scrollingElement)\n            ? $(document.defaultView)\n            : $scrollingElement;\n    },\n    /**\n     * @return {boolean}\n     */\n    hasScrollableContent() {\n        return this[0].scrollHeight > this[0].clientHeight;\n    },\n    /**\n     * @returns {boolean}\n     */\n    isScrollable() {\n        if (!this.length) {\n            return false;\n        }\n        const overflow = this.css('overflow-y');\n        const el = this[0];\n        return overflow === 'auto' || overflow === 'scroll'\n            || (overflow === 'visible' && el === el.ownerDocument.scrollingElement);\n    },\n});\n\n// jQuery functions monkey-patching\n\n// Some magic to ensure scrollTop and animate on html/body animate the top level\n// scrollable element even if not html or body. Note: we should consider\n// removing this as it was only really needed when the #wrapwrap was the one\n// with the scrollbar. Although the rest of the code still use\n// getScrollingElement to be generic so this is consistent. Maybe all of this\n// can live on as long as we continue using jQuery a lot. We can decide of the\n// fate of getScrollingElement and related code the moment we get rid of jQuery.\nconst originalScrollTop = $.fn.scrollTop;\n$.fn.scrollTop = function (value) {\n    if (value !== undefined && this.filter('html, body').length) {\n        // The caller wants to scroll a set of elements including html and/or\n        // body to a specific point -> do that but make sure to add the real\n        // top level element to that set of elements if any different is found.\n        const $withRealScrollable = this.not('html, body').add($().getScrollingElement(this[0].ownerDocument));\n        originalScrollTop.apply($withRealScrollable, arguments);\n        return this;\n    } else if (value === undefined && this.eq(0).is('html, body')) {\n        // The caller wants to get the scroll point of a set of elements, jQuery\n        // will return the scroll point of the first one, if it is html or body\n        // return the scroll point of the real top level element.\n        return originalScrollTop.apply($().getScrollingElement(this[0].ownerDocument), arguments);\n    }\n    return originalScrollTop.apply(this, arguments);\n};\nconst originalAnimate = $.fn.animate;\n$.fn.animate = function (properties, ...rest) {\n    const props = Object.assign({}, properties);\n    if ('scrollTop' in props && this.filter('html, body').length) {\n        // The caller wants to scroll a set of elements including html and/or\n        // body to a specific point -> do that but make sure to add the real\n        // top level element to that set of elements if any different is found.\n        const $withRealScrollable = this.not('html, body').add($().getScrollingElement(this[0].ownerDocument));\n        originalAnimate.call($withRealScrollable, {'scrollTop': props['scrollTop']}, ...rest);\n        delete props['scrollTop'];\n    }\n    if (!Object.keys(props).length) {\n        return this;\n    }\n    return originalAnimate.call(this, props, ...rest);\n};\n", "/**\n * Improved John Resig's inheritance, based on:\n *\n * Simple JavaScript Inheritance\n * By John Resig http://ejohn.org/\n * MIT Licensed.\n *\n * Adds \"include()\"\n *\n * Defines The Class object. That object can be used to define and inherit classes using\n * the extend() method.\n *\n * Example::\n *\n *     var Person = Class.extend({\n *      init: function(isDancing){\n *         this.dancing = isDancing;\n *       },\n *       dance: function(){\n *         return this.dancing;\n *       }\n *     });\n *\n * The init() method act as a constructor. This class can be instanced this way::\n *\n *     var person = new Person(true);\n *     person.dance();\n *\n *     The Person class can also be extended again:\n *\n *     var Ninja = Person.extend({\n *       init: function(){\n *         this._super( false );\n *       },\n *       dance: function(){\n *         // Call the inherited version of dance()\n *         return this._super();\n *       },\n *       swingSword: function(){\n *         return true;\n *       }\n *     });\n *\n * When extending a class, each re-defined method can use this._super() to call the previous\n * implementation of that method.\n *\n * @class Class\n */\nfunction OdooClass(){}\n\nvar initializing = false;\n// eslint-disable-next-line no-undef\nvar fnTest = /xyz/.test(function(){xyz();}) ? /\\b_super\\b/ : /.*/;\n\n/**\n * Subclass an existing class\n *\n * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class\n */\nOdooClass.extend = function() {\n    var _super = this.prototype;\n    // Support mixins arguments\n    var args = [...arguments];\n    args.unshift({});\n\n    const prop = {};\n    args.forEach((arg) => {\n        Object.assign(prop, arg);\n    });\n\n    // Instantiate a web class (but only create the instance,\n    // don't run the init constructor)\n    initializing = true;\n    var This = this;\n    var prototype = new This();\n    initializing = false;\n\n    // Copy the properties over onto the new prototype\n    Object.keys(prop).forEach((name) => {\n        // Check if we're overwriting an existing function\n        prototype[name] = typeof prop[name] == \"function\" &&\n                          fnTest.test(prop[name]) ?\n                (function(name, fn) {\n                    return function() {\n                        var tmp = this._super;\n\n                        // Add a new ._super() method that is the same\n                        // method but on the super-class\n                        this._super = _super[name];\n\n                        // The method only need to be bound temporarily, so\n                        // we remove it when we're done executing\n                        var ret = fn.apply(this, arguments);\n                        this._super = tmp;\n\n                        return ret;\n                    };\n                })(name, prop[name]) :\n                prop[name];\n    });\n\n    // The dummy class constructor\n    function Class() {\n        if(this.constructor !== OdooClass){\n            throw new Error(\"You can only instanciate objects with the 'new' operator\");\n        }\n        // All construction is actually done in the init method\n        this._super = null;\n        if (!initializing && this.init) {\n            var ret = this.init.apply(this, arguments);\n            if (ret) { return ret; }\n        }\n        return this;\n    }\n    Class.include = function (properties) {\n        Object.keys(properties).forEach((name) => {\n            if (typeof properties[name] !== 'function'\n                    || !fnTest.test(properties[name])) {\n                prototype[name] = properties[name];\n            } else if (typeof prototype[name] === 'function'\n                       && prototype.hasOwnProperty(name)) {\n                prototype[name] = (function (name, fn, previous) {\n                    return function () {\n                        var tmp = this._super;\n                        this._super = previous;\n                        var ret = fn.apply(this, arguments);\n                        this._super = tmp;\n                        return ret;\n                    };\n                })(name, properties[name], prototype[name]);\n            } else if (typeof _super[name] === 'function') {\n                prototype[name] = (function (name, fn) {\n                    return function () {\n                        var tmp = this._super;\n                        this._super = _super[name];\n                        var ret = fn.apply(this, arguments);\n                        this._super = tmp;\n                        return ret;\n                    };\n                })(name, properties[name]);\n            }\n        });\n    };\n\n    // Populate our constructed prototype object\n    Class.prototype = prototype;\n\n    // Enforce the constructor to be what we expect\n    Class.constructor = Class;\n\n    // And make this class extendable\n    Class.extend = this.extend;\n\n    return Class;\n};\n\nexport default OdooClass;\n", "import { App, EventBus } from \"@odoo/owl\";\nimport { SERVICES_METADATA } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { session } from \"@web/session\";\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * @typedef {Object} OdooEnv\n * @property {import(\"services\").Services} services\n * @property {EventBus} bus\n * @property {string} debug\n * @property {(str: string) => string} _t\n * @property {boolean} [isSmall]\n */\n\n// -----------------------------------------------------------------------------\n// makeEnv\n// -----------------------------------------------------------------------------\n\n/**\n * Return a value Odoo Env object\n *\n * @returns {OdooEnv}\n */\nexport function makeEnv() {\n    return {\n        bus: new EventBus(),\n        services: {},\n        debug: odoo.debug,\n        get isSmall() {\n            throw new Error(\"UI service not initialized!\");\n        },\n    };\n}\n\n// -----------------------------------------------------------------------------\n// Service Launcher\n// -----------------------------------------------------------------------------\n\nconst serviceRegistry = registry.category(\"services\");\n\nserviceRegistry.addValidation({\n    start: Function,\n    dependencies: { type: Array, element: String, optional: true },\n    async: { type: [{ type: Array, element: String }, { value: true }], optional: true },\n    \"*\": true,\n});\n\nlet startServicesPromise = null;\n\n/**\n * Start all services registered in the service registry, while making sure\n * each service dependencies are properly fulfilled.\n *\n * @param {OdooEnv} env\n * @returns {Promise<void>}\n */\nexport async function startServices(env) {\n    // Wait for all synchronous code so that if new services that depend on\n    // one another are added to the registry, they're all present before we\n    // start them regardless of the order they're added to the registry.\n    await Promise.resolve();\n\n    const toStart = new Map();\n    serviceRegistry.addEventListener(\"UPDATE\", async (ev) => {\n        // Wait for all synchronous code so that if new services that depend on\n        // one another are added to the registry, they're all present before we\n        // start them regardless of the order they're added to the registry.\n        await Promise.resolve();\n        const { operation, key: name, value: service } = ev.detail;\n        if (operation === \"delete\") {\n            // We hardly see why it would be usefull to remove a service.\n            // Furthermore we could encounter problems with dependencies.\n            // Keep it simple!\n            return;\n        }\n        if (toStart.size) {\n            const namedService = Object.assign(Object.create(service), { name });\n            toStart.set(name, namedService);\n        } else {\n            await _startServices(env, toStart);\n        }\n    });\n    await _startServices(env, toStart);\n}\n\nasync function _startServices(env, toStart) {\n    if (startServicesPromise) {\n        return startServicesPromise.then(() => _startServices(env, toStart));\n    }\n    const services = env.services;\n    for (const [name, service] of serviceRegistry.getEntries()) {\n        if (!(name in services)) {\n            const namedService = Object.assign(Object.create(service), { name });\n            toStart.set(name, namedService);\n        }\n    }\n\n    // start as many services in parallel as possible\n    async function start() {\n        let service = null;\n        const proms = [];\n        while ((service = findNext())) {\n            const name = service.name;\n            toStart.delete(name);\n            const entries = (service.dependencies || []).map((dep) => [dep, services[dep]]);\n            const dependencies = Object.fromEntries(entries);\n            if (name in services) {\n                continue;\n            }\n            const value = service.start(env, dependencies);\n            if (\"async\" in service) {\n                SERVICES_METADATA[name] = service.async;\n            }\n            proms.push(\n                Promise.resolve(value).then((val) => {\n                    services[name] = val || null;\n                })\n            );\n        }\n        await Promise.all(proms);\n        if (proms.length) {\n            return start();\n        }\n    }\n    startServicesPromise = start().finally(() => {\n        startServicesPromise = null;\n    });\n    await startServicesPromise;\n    if (toStart.size) {\n        const missingDeps = new Set();\n        for (const service of toStart.values()) {\n            for (const dependency of service.dependencies) {\n                if (!(dependency in services) && !toStart.has(dependency)) {\n                    missingDeps.add(dependency);\n                }\n            }\n        }\n        const depNames = [...missingDeps].join(\", \");\n        throw new Error(\n            `Some services could not be started: ${[\n                ...toStart.keys(),\n            ]}. Missing dependencies: ${depNames}`\n        );\n    }\n\n    function findNext() {\n        for (const s of toStart.values()) {\n            if (s.dependencies) {\n                if (s.dependencies.every((d) => d in services)) {\n                    return s;\n                }\n            } else {\n                return s;\n            }\n        }\n        return null;\n    }\n}\n\n/**\n * Create an application with a given component as root and mount it. If no env\n * is provided, the application will be treated as a \"root\": an env will be\n * created and the services will be started, it will also be set as the root\n * in `__WOWL_DEBUG__`\n *\n * @param {import(\"@odoo/owl\").Component} component the component to mount\n * @param {HTMLElement} target the HTML element in which to mount the app\n * @param {Partial<ConstructorParameters<typeof App>[1]>} [appConfig] object\n *  containing a (partial) config for the app.\n */\nexport async function mountComponent(component, target, appConfig = {}) {\n    let { env } = appConfig;\n    const isRoot = !env;\n    if (isRoot) {\n        env = await makeEnv();\n        await startServices(env);\n    }\n    const app = new App(component, {\n        env,\n        getTemplate,\n        dev: env.debug || session.test_mode,\n        warnIfNoStaticProps: !session.test_mode,\n        name: component.constructor.name,\n        translatableAttributes: [\"data-tooltip\"],\n        translateFn: _t,\n        ...appConfig,\n    });\n    const root = await app.mount(target);\n    if (isRoot) {\n        odoo.__WOWL_DEBUG__ = { root };\n    }\n    return app;\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { clamp } from \"@web/core/utils/numbers\";\n\nimport { Component, onMounted, onWillUnmount, useRef, useState } from \"@odoo/owl\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\nconst isScrollSwipable = (scrollables) => {\n    return {\n        left: !scrollables.filter((e) => e.scrollLeft !== 0).length,\n        right: !scrollables.filter(\n            (e) => e.scrollLeft + Math.round(e.getBoundingClientRect().width) !== e.scrollWidth\n        ).length,\n    };\n};\n\n/**\n * Action Swiper\n *\n * This component is intended to perform action once a user has completed a touch swipe.\n * You can choose the direction allowed for such behavior (left, right or both).\n * The action to perform must be passed as a props. It is possible to define a condition\n * to allow the swipe interaction conditionnally.\n * @extends Component\n */\nexport class ActionSwiper extends Component {\n    static template = \"web.ActionSwiper\";\n    static props = {\n        onLeftSwipe: {\n            type: Object,\n            args: {\n                action: Function,\n                icon: String,\n                bgColor: String,\n            },\n            optional: true,\n        },\n        onRightSwipe: {\n            type: Object,\n            args: {\n                action: Function,\n                icon: String,\n                bgColor: String,\n            },\n            optional: true,\n        },\n        slots: Object,\n        animationOnMove: { type: Boolean, optional: true },\n        animationType: { type: String, optional: true },\n        swipeDistanceRatio: { type: Number, optional: true },\n        swipeInvalid: { type: Function, optional: true },\n    };\n\n    static defaultProps = {\n        onLeftSwipe: undefined,\n        onRightSwipe: undefined,\n        animationOnMove: true,\n        animationType: \"bounce\",\n        swipeDistanceRatio: 2,\n    };\n\n    setup() {\n        this.actionTimeoutId = null;\n        this.resetTimeoutId = null;\n        this.defaultState = {\n            containerStyle: \"\",\n            isSwiping: false,\n            width: undefined,\n        };\n        this.root = useRef(\"root\");\n        this.targetContainer = useRef(\"targetContainer\");\n        this.state = useState({ ...this.defaultState });\n        this.scrollables = undefined;\n        this.startX = undefined;\n        this.swipedDistance = 0;\n        this.isScrollValidated = false;\n        onMounted(() => {\n            if (this.targetContainer.el) {\n                this.state.width = this.targetContainer.el.getBoundingClientRect().width;\n            }\n            // Forward classes set on component to slot, as we only want to wrap an\n            // existing component without altering the DOM structure any more than\n            // strictly necessary\n            if (this.props.onLeftSwipe || this.props.onRightSwipe) {\n                const classes = new Set(this.root.el.classList);\n                classes.delete(\"o_actionswiper\");\n                for (const className of classes) {\n                    this.targetContainer.el.firstChild.classList.add(className);\n                    this.root.el.classList.remove(className);\n                }\n            }\n        });\n        onWillUnmount(() => {\n            browser.clearTimeout(this.actionTimeoutId);\n            browser.clearTimeout(this.resetTimeoutId);\n        });\n    }\n    get localizedProps() {\n        return {\n            onLeftSwipe:\n                localization.direction === \"rtl\" ? this.props.onRightSwipe : this.props.onLeftSwipe,\n            onRightSwipe:\n                localization.direction === \"rtl\" ? this.props.onLeftSwipe : this.props.onRightSwipe,\n        };\n    }\n\n    /**\n     * @private\n     * @param {TouchEvent} ev\n     */\n    _onTouchEndSwipe() {\n        if (this.state.isSwiping) {\n            this.state.isSwiping = false;\n            if (\n                this.localizedProps.onRightSwipe &&\n                this.swipedDistance > this.state.width / this.props.swipeDistanceRatio\n            ) {\n                this.swipedDistance = this.state.width;\n                this.handleSwipe(this.localizedProps.onRightSwipe.action);\n            } else if (\n                this.localizedProps.onLeftSwipe &&\n                this.swipedDistance < -this.state.width / this.props.swipeDistanceRatio\n            ) {\n                this.swipedDistance = -this.state.width;\n                this.handleSwipe(this.localizedProps.onLeftSwipe.action);\n            } else {\n                this.state.containerStyle = \"\";\n            }\n        }\n    }\n    /**\n     * @private\n     * @param {TouchEvent} ev\n     */\n    _onTouchMoveSwipe(ev) {\n        if (this.state.isSwiping) {\n            if (this.props.swipeInvalid && this.props.swipeInvalid()) {\n                this.state.isSwiping = false;\n                return;\n            }\n            const { onLeftSwipe, onRightSwipe } = this.localizedProps;\n            this.swipedDistance = clamp(\n                ev.touches[0].clientX - this.startX,\n                onLeftSwipe ? -this.state.width : 0,\n                onRightSwipe ? this.state.width : 0\n            );\n            // Prevent the browser to navigate back/forward when using swipe\n            // gestures while still allowing to scroll vertically.\n            if (Math.abs(this.swipedDistance) > 40) {\n                ev.preventDefault();\n            }\n            // If there are scrollable elements under touch pressure,\n            // they must be at their limits to allow swiping.\n            if (\n                !this.isScrollValidated &&\n                this.scrollables &&\n                !isScrollSwipable(this.scrollables)[this.swipedDistance > 0 ? \"left\" : \"right\"]\n            ) {\n                return this._reset();\n            }\n            this.isScrollValidated = true;\n\n            if (this.props.animationOnMove) {\n                this.state.containerStyle = `transform: translateX(${this.swipedDistance}px)`;\n            }\n        }\n    }\n    /**\n     * @private\n     * @param {TouchEvent} ev\n     */\n    _onTouchStartSwipe(ev) {\n        this.scrollables = ev\n            .composedPath()\n            .filter(\n                (e) =>\n                    e.nodeType === 1 &&\n                    this.targetContainer.el.contains(e) &&\n                    e.scrollWidth > e.getBoundingClientRect().width &&\n                    [\"auto\", \"scroll\"].includes(window.getComputedStyle(e)[\"overflow-x\"])\n            );\n        if (!this.state.width) {\n            this.state.width =\n                this.targetContainer && this.targetContainer.el.getBoundingClientRect().width;\n        }\n        this.state.isSwiping = true;\n        this.isScrollValidated = false;\n        this.startX = ev.touches[0].clientX;\n    }\n\n    /**\n     * @private\n     */\n    _reset() {\n        Object.assign(this.state, { ...this.defaultState });\n        this.scrollables = undefined;\n        this.startX = undefined;\n        this.swipedDistance = 0;\n        this.isScrollValidated = false;\n    }\n\n    handleSwipe(action) {\n        if (this.props.animationType === \"bounce\") {\n            this.state.containerStyle = `transform: translateX(${this.swipedDistance}px)`;\n            this.actionTimeoutId = browser.setTimeout(async () => {\n                await action(Promise.resolve());\n                this._reset();\n            }, 500);\n        } else if (this.props.animationType === \"forwards\") {\n            this.state.containerStyle = `transform: translateX(${this.swipedDistance}px)`;\n            this.actionTimeoutId = browser.setTimeout(async () => {\n                const prom = new Deferred();\n                await action(prom);\n                this.state.isSwiping = true;\n                this.state.containerStyle = `transform: translateX(${-this.swipedDistance}px)`;\n                this.resetTimeoutId = browser.setTimeout(() => {\n                    prom.resolve();\n                    this._reset();\n                }, 100);\n            }, 100);\n        } else {\n            return action(Promise.resolve());\n        }\n    }\n}\n", "import { browser } from \"./browser/browser\";\n\nbrowser.addEventListener(\"click\", (ev) => {\n    const href = ev.target.closest(\"a\")?.getAttribute(\"href\");\n    if (href && href === \"#\") {\n        ev.preventDefault(); // single hash in href are just a way to activate A-tags node\n        return;\n    }\n});\n", "import { browser } from \"./browser/browser\";\nimport { registry } from \"./registry\";\nimport { session } from \"@web/session\";\nimport { Component, xml, onWillStart, whenReady } from \"@odoo/owl\";\n\nconst computeCacheMap = () => {\n    for (const script of document.head.querySelectorAll(\"script[src]\")) {\n        cacheMap.set(script.src, Promise.resolve(true));\n    }\n    for (const link of document.head.querySelectorAll(\"link[rel=stylesheet][href]\")) {\n        cacheMap.set(link.href, Promise.resolve(true));\n    }\n};\n\n/**\n * @param {HTMLLinkElement | HTMLScriptElement} el\n * @param {(event: Event) => any} onLoad\n * @param {(error: Error) => any} onError\n */\nconst onLoadAndError = (el, onLoad, onError) => {\n    const onLoadListener = (event) => {\n        removeListeners();\n        onLoad(event);\n    };\n\n    const onErrorListener = (error) => {\n        removeListeners();\n        onError(error);\n    };\n\n    const removeListeners = () => {\n        el.removeEventListener(\"load\", onLoadListener);\n        el.removeEventListener(\"error\", onErrorListener);\n    };\n\n    el.addEventListener(\"load\", onLoadListener);\n    el.addEventListener(\"error\", onErrorListener);\n};\n\n/**\n * This export is done only in order to modify the behavior of the exported\n * functions. This is done in order to be able to make a test environment.\n * Modules should only use the methods exported below.\n */\nexport const assets = {\n    retries: {\n        count: 3,\n        delay: 5000,\n        extraDelay: 2500,\n    },\n};\n\nconst cacheMap = new Map();\n\nwhenReady(computeCacheMap);\n\nexport class AssetsLoadingError extends Error {}\n\n/**\n * Loads the given url inside a script tag.\n *\n * @param {string} url the url of the script\n * @returns {Promise<true>} resolved when the script has been loaded\n */\nassets.loadJS = async function loadJS(url) {\n    if (cacheMap.has(url)) {\n        return cacheMap.get(url);\n    }\n    const scriptEl = document.createElement(\"script\");\n    scriptEl.type = url.includes(\"web/static/lib/pdfjs/\") ? \"module\" : \"text/javascript\";\n    scriptEl.src = url;\n    const promise = new Promise((resolve, reject) => {\n        onLoadAndError(scriptEl, resolve, () => {\n            cacheMap.delete(url);\n            reject(new AssetsLoadingError(`The loading of ${url} failed`));\n        });\n    });\n    cacheMap.set(url, promise);\n    document.head.appendChild(scriptEl);\n    return promise;\n};\n\n/**\n * Loads the given url as a stylesheet.\n *\n * @param {string} url the url of the stylesheet\n * @returns {Promise<true>} resolved when the stylesheet has been loaded\n */\nassets.loadCSS = async function loadCSS(url, retryCount = 0) {\n    if (cacheMap.has(url)) {\n        return cacheMap.get(url);\n    }\n    const linkEl = document.createElement(\"link\");\n    linkEl.type = \"text/css\";\n    linkEl.rel = \"stylesheet\";\n    linkEl.href = url;\n    const promise = new Promise((resolve, reject) => {\n        const onError = (...args) => {\n            cacheMap.delete(url);\n            return reject(...args);\n        };\n\n        onLoadAndError(linkEl, resolve, async () => {\n            cacheMap.delete(url);\n            if (retryCount < assets.retries.count) {\n                await new Promise((resolve) =>\n                    setTimeout(\n                        resolve,\n                        assets.retries.delay + assets.retries.extraDelay * retryCount\n                    )\n                );\n                linkEl.remove();\n                loadCSS(url, retryCount + 1)\n                    .then(resolve)\n                    .catch(onError);\n            } else {\n                onError(new AssetsLoadingError(`The loading of ${url} failed`));\n            }\n        });\n    });\n    cacheMap.set(url, promise);\n    document.head.appendChild(linkEl);\n    return promise;\n};\n\n/**\n * Get the files information as descriptor object from a public asset template.\n *\n * @param {string} bundleName Name of the bundle containing the list of files\n * @returns {Promise<{cssLibs, jsLibs}>}\n */\nassets.getBundle = async function getBundle(bundleName) {\n    if (!cacheMap.has(bundleName)) {\n        const url = new URL(`/web/bundle/${bundleName}`, location.origin);\n        for (const [key, value] of Object.entries(session.bundle_params || {})) {\n            url.searchParams.set(key, value);\n        }\n        const promise = new Promise((resolve, reject) => {\n            browser\n                .fetch(url.href)\n                .then((response) => {\n                    return response.json().then((json) => {\n                        const assets = {\n                            cssLibs: [],\n                            jsLibs: [],\n                        };\n                        for (const key in json) {\n                            const file = json[key];\n                            if (file.type === \"link\" && file.src) {\n                                assets.cssLibs.push(file.src);\n                            } else if (file.type === \"script\" && file.src) {\n                                assets.jsLibs.push(file.src);\n                            }\n                        }\n                        resolve(assets);\n                    });\n                })\n                .catch((...args) => {\n                    cacheMap.delete(bundleName);\n                    reject(...args);\n                });\n        });\n        cacheMap.set(bundleName, promise);\n    }\n    return cacheMap.get(bundleName);\n};\n\n/**\n * Loads the given js/css libraries and asset bundles. Note that no library or\n * asset will be loaded if it was already done before.\n *\n * @param {string} bundleName\n * @returns {Promise[]}\n */\nassets.loadBundle = async function loadBundle(bundleName) {\n    if (typeof bundleName === \"string\") {\n        const desc = await assets.getBundle(bundleName);\n        return Promise.all([\n            ...(desc.cssLibs || []).map(assets.loadCSS),\n            ...(desc.jsLibs || []).map(assets.loadJS),\n        ]);\n    } else {\n        throw new Error(\n            `loadBundle(bundleName:string) accepts only bundleName argument as a string ! Not ${JSON.stringify(\n                bundleName\n            )} as ${typeof bundleName}`\n        );\n    }\n};\n\nexport const loadJS = function (url) {\n    return assets.loadJS(url);\n};\nexport const loadCSS = function (url) {\n    return assets.loadCSS(url);\n};\nexport const getBundle = function (bundleName) {\n    return assets.getBundle(bundleName);\n};\nexport const loadBundle = function (bundleName) {\n    return assets.loadBundle(bundleName);\n};\n\n/**\n * Utility component that loads an asset bundle before instanciating a component\n */\nexport class LazyComponent extends Component {\n    static template = xml`<t t-component=\"Component\" t-props=\"props.props\"/>`;\n    static props = {\n        Component: String,\n        bundle: String,\n        props: { type: Object, optional: true },\n    };\n    setup() {\n        onWillStart(async () => {\n            await loadBundle(this.props.bundle);\n            this.Component = registry.category(\"lazy_components\").get(this.props.Component);\n        });\n    }\n}\n", "import { Deferred } from \"@web/core/utils/concurrency\";\nimport { useAutofocus, useForwardRefToParent, useService } from \"@web/core/utils/hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { usePosition } from \"@web/core/position/position_hook\";\nimport { Component, onWillUpdateProps, useExternalListener, useRef, useState } from \"@odoo/owl\";\n\nexport class AutoComplete extends Component {\n    static template = \"web.AutoComplete\";\n    static props = {\n        value: { type: String, optional: true },\n        id: { type: String, optional: true },\n        onSelect: { type: Function },\n        sources: {\n            type: Array,\n            element: {\n                type: Object,\n                shape: {\n                    placeholder: { type: String, optional: true },\n                    optionTemplate: { type: String, optional: true },\n                    options: [Array, Function],\n                },\n            },\n        },\n        placeholder: { type: String, optional: true },\n        autoSelect: { type: Boolean, optional: true },\n        resetOnSelect: { type: Boolean, optional: true },\n        onInput: { type: Function, optional: true },\n        onCancel: { type: Function, optional: true },\n        onChange: { type: Function, optional: true },\n        onBlur: { type: Function, optional: true },\n        onFocus: { type: Function, optional: true },\n        input: { type: Function, optional: true },\n        dropdown: { type: Boolean, optional: true },\n        autofocus: { type: Boolean, optional: true },\n        class: { type: String, optional: true },\n    };\n    static defaultProps = {\n        value: \"\",\n        placeholder: \"\",\n        autoSelect: false,\n        dropdown: true,\n        onInput: () => {},\n        onCancel: () => {},\n        onChange: () => {},\n        onBlur: () => {},\n        onFocus: () => {},\n    };\n\n    setup() {\n        this.nextSourceId = 0;\n        this.nextOptionId = 0;\n        this.sources = [];\n        this.inEdition = false;\n        this.timeout = 250;\n\n        this.state = useState({\n            navigationRev: 0,\n            optionsRev: 0,\n            open: false,\n            activeSourceOption: null,\n            value: this.props.value,\n        });\n\n        this.inputRef = useForwardRefToParent(\"input\");\n        if (this.props.autofocus) {\n            useAutofocus({ refName: \"input\" });\n        }\n        this.root = useRef(\"root\");\n\n        this.debouncedProcessInput = useDebounced(async () => {\n            const currentPromise = this.pendingPromise;\n            this.pendingPromise = null;\n            this.props.onInput({\n                inputValue: this.inputRef.el.value,\n            });\n            try {\n                await this.open(true);\n                currentPromise.resolve();\n            } catch {\n                currentPromise.reject();\n            } finally {\n                if (currentPromise === this.loadingPromise) {\n                    this.loadingPromise = null;\n                }\n            }\n        }, this.timeout);\n\n        useExternalListener(window, \"scroll\", this.externalClose, true);\n        useExternalListener(window, \"pointerdown\", this.externalClose, true);\n\n        this.hotkey = useService(\"hotkey\");\n        this.hotkeysToRemove = [];\n\n        onWillUpdateProps((nextProps) => {\n            if (this.props.value !== nextProps.value || this.forceValFromProp) {\n                this.forceValFromProp = false;\n                if (!this.inEdition) {\n                    this.state.value = nextProps.value;\n                    this.inputRef.el.value = nextProps.value;\n                }\n                this.close();\n            }\n        });\n\n        // position and size\n        if (this.props.dropdown) {\n            usePosition(\"sourcesList\", () => this.targetDropdown, this.dropdownOptions);\n        } else {\n            this.open(false);\n        }\n    }\n\n    get targetDropdown() {\n        return this.inputRef.el;\n    }\n\n    get activeSourceOptionId() {\n        if (!this.isOpened || !this.state.activeSourceOption) {\n            return undefined;\n        }\n        const [sourceIndex, optionIndex] = this.state.activeSourceOption;\n        const source = this.sources[sourceIndex];\n        return `${this.props.id || \"autocomplete\"}_${sourceIndex}_${source.isLoading ? \"loading\" : optionIndex}`;\n    }\n\n    get dropdownOptions() {\n        return {\n            position: \"bottom-start\",\n        };\n    }\n\n    get isOpened() {\n        return this.state.open;\n    }\n\n    get hasOptions() {\n        for (const source of this.sources) {\n            if (source.isLoading || source.options.length) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    open(useInput = false) {\n        this.state.open = true;\n        return this.loadSources(useInput);\n    }\n\n    close() {\n        this.state.open = false;\n        this.state.activeSourceOption = null;\n    }\n\n    cancel() {\n        if (this.inputRef.el.value.length) {\n            if (this.props.autoSelect) {\n                this.inputRef.el.value = this.props.value;\n                this.props.onCancel();\n            }\n        }\n        this.close();\n    }\n\n    async loadSources(useInput) {\n        this.sources = [];\n        this.state.activeSourceOption = null;\n        const proms = [];\n        for (const pSource of this.props.sources) {\n            const source = this.makeSource(pSource);\n            this.sources.push(source);\n\n            const options = this.loadOptions(\n                pSource.options,\n                useInput ? this.inputRef.el.value.trim() : \"\"\n            );\n            if (options instanceof Promise) {\n                source.isLoading = true;\n                const prom = options.then((options) => {\n                    source.options = options.map((option) => this.makeOption(option));\n                    source.isLoading = false;\n                    this.state.optionsRev++;\n                });\n                proms.push(prom);\n            } else {\n                source.options = options.map((option) => this.makeOption(option));\n            }\n        }\n\n        await Promise.all(proms);\n        this.navigate(0);\n    }\n    get displayOptions() {\n        return !this.props.dropdown || (this.isOpened && this.hasOptions);\n    }\n    loadOptions(options, request) {\n        if (typeof options === \"function\") {\n            return options(request);\n        } else {\n            return options;\n        }\n    }\n    makeOption(option) {\n        return Object.assign(Object.create(option), {\n            id: ++this.nextOptionId,\n        });\n    }\n    makeSource(source) {\n        return {\n            id: ++this.nextSourceId,\n            options: [],\n            isLoading: false,\n            placeholder: source.placeholder,\n            optionTemplate: source.optionTemplate,\n        };\n    }\n\n    isActiveSourceOption([sourceIndex, optionIndex]) {\n        return (\n            this.state.activeSourceOption &&\n            this.state.activeSourceOption[0] === sourceIndex &&\n            this.state.activeSourceOption[1] === optionIndex\n        );\n    }\n    selectOption(indices, params = {}) {\n        const option = this.sources[indices[0]].options[indices[1]];\n        this.inEdition = false;\n        if (option.unselectable) {\n            this.inputRef.el.value = \"\";\n            this.close();\n            return;\n        }\n\n        if (this.props.resetOnSelect) {\n            this.inputRef.el.value = \"\";\n        }\n\n        this.forceValFromProp = true;\n        this.props.onSelect(option, {\n            ...params,\n            input: this.inputRef.el,\n        });\n        this.close();\n    }\n\n    navigate(direction) {\n        let step = Math.sign(direction);\n        if (!step) {\n            this.state.activeSourceOption = null;\n            step = 1;\n        } else {\n            this.state.navigationRev++;\n        }\n\n        if (this.state.activeSourceOption) {\n            let [sourceIndex, optionIndex] = this.state.activeSourceOption;\n            let source = this.sources[sourceIndex];\n\n            optionIndex += step;\n            if (0 > optionIndex || optionIndex >= source.options.length) {\n                sourceIndex += step;\n                source = this.sources[sourceIndex];\n\n                while (source && source.isLoading) {\n                    sourceIndex += step;\n                    source = this.sources[sourceIndex];\n                }\n\n                if (source) {\n                    optionIndex = step < 0 ? source.options.length - 1 : 0;\n                }\n            }\n\n            this.state.activeSourceOption = source ? [sourceIndex, optionIndex] : null;\n        } else {\n            let sourceIndex = step < 0 ? this.sources.length - 1 : 0;\n            let source = this.sources[sourceIndex];\n\n            while (source && source.isLoading) {\n                sourceIndex += step;\n                source = this.sources[sourceIndex];\n            }\n\n            if (source) {\n                const optionIndex = step < 0 ? source.options.length - 1 : 0;\n                if (optionIndex < source.options.length) {\n                    this.state.activeSourceOption = [sourceIndex, optionIndex];\n                }\n            }\n        }\n    }\n\n    onInputBlur() {\n        if (this.ignoreBlur) {\n            this.ignoreBlur = false;\n            return;\n        }\n        this.props.onBlur({\n            inputValue: this.inputRef.el.value,\n        });\n        this.inEdition = false;\n    }\n    onInputClick() {\n        if (!this.isOpened) {\n            this.open(this.inputRef.el.value.trim() !== this.props.value);\n        } else {\n            this.close();\n        }\n    }\n    onInputChange(ev) {\n        if (this.ignoreBlur) {\n            ev.stopImmediatePropagation();\n        }\n        this.props.onChange({\n            inputValue: this.inputRef.el.value,\n        });\n    }\n    async onInput() {\n        this.inEdition = true;\n        this.pendingPromise = this.pendingPromise || new Deferred();\n        this.loadingPromise = this.pendingPromise;\n        this.debouncedProcessInput();\n    }\n\n    onInputFocus(ev) {\n        this.inputRef.el.setSelectionRange(0, this.inputRef.el.value.length);\n        this.props.onFocus(ev);\n    }\n\n    get autoCompleteRootClass() {\n        let classList = \"\";\n        if (this.props.class) {\n            classList += this.props.class;\n        }\n        if (this.props.dropdown) {\n            classList += \" dropdown\";\n        }\n        return classList;\n    }\n\n    get ulDropdownClass() {\n        let classList = \"\";\n        if (this.props.dropdown) {\n            classList += \" dropdown-menu ui-autocomplete\";\n        } else {\n            classList += \" list-group\";\n        }\n        return classList;\n    }\n\n    async onInputKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        const isSelectKey = hotkey === \"enter\" || hotkey === \"tab\";\n\n        if (this.loadingPromise && isSelectKey) {\n            if (hotkey === \"enter\") {\n                ev.stopPropagation();\n                ev.preventDefault();\n            }\n\n            await this.loadingPromise;\n        }\n\n        switch (hotkey) {\n            case \"enter\":\n                if (!this.isOpened || !this.state.activeSourceOption) {\n                    return;\n                }\n                this.selectOption(this.state.activeSourceOption);\n                break;\n            case \"escape\":\n                if (!this.isOpened) {\n                    return;\n                }\n                this.cancel();\n                break;\n            case \"tab\":\n                if (!this.isOpened) {\n                    return;\n                }\n                if (\n                    this.props.autoSelect &&\n                    this.state.activeSourceOption &&\n                    (this.state.navigationRev > 0 || this.inputRef.el.value.length > 0)\n                ) {\n                    this.selectOption(this.state.activeSourceOption);\n                }\n                this.close();\n                return;\n            case \"arrowup\":\n                this.navigate(-1);\n                if (!this.isOpened) {\n                    this.open(true);\n                }\n                break;\n            case \"arrowdown\":\n                this.navigate(+1);\n                if (!this.isOpened) {\n                    this.open(true);\n                }\n                break;\n            default:\n                return;\n        }\n\n        ev.stopPropagation();\n        ev.preventDefault();\n    }\n\n    onOptionMouseEnter(indices) {\n        this.state.activeSourceOption = indices;\n    }\n    onOptionMouseLeave() {\n        this.state.activeSourceOption = null;\n    }\n    onOptionClick(indices) {\n        this.selectOption(indices);\n        this.inputRef.el.focus();\n    }\n\n    externalClose(ev) {\n        if (this.isOpened && !this.root.el.contains(ev.target)) {\n            this.cancel();\n        }\n    }\n}\n", "/**\n * Builder for BarcodeDetector-like polyfill class using ZXing library.\n *\n * @param {ZXing} ZXing Zxing library\n * @returns {class} ZxingBarcodeDetector class\n */\nexport function buildZXingBarcodeDetector(ZXing) {\n    const ZXingFormats = new Map([\n        [\"aztec\", ZXing.BarcodeFormat.AZTEC],\n        [\"code_39\", ZXing.BarcodeFormat.CODE_39],\n        [\"code_128\", ZXing.BarcodeFormat.CODE_128],\n        [\"data_matrix\", ZXing.BarcodeFormat.DATA_MATRIX],\n        [\"ean_8\", ZXing.BarcodeFormat.EAN_8],\n        [\"ean_13\", ZXing.BarcodeFormat.EAN_13],\n        [\"itf\", ZXing.BarcodeFormat.ITF],\n        [\"pdf417\", ZXing.BarcodeFormat.PDF_417],\n        [\"qr_code\", ZXing.BarcodeFormat.QR_CODE],\n        [\"upc_a\", ZXing.BarcodeFormat.UPC_A],\n        [\"upc_e\", ZXing.BarcodeFormat.UPC_E],\n    ]);\n\n    const allSupportedFormats = Array.from(ZXingFormats.keys());\n\n    /**\n     * Restore previous behavior of the lib because since https://github.com/zxing-js/library/commit/7644e279df9fd2e754e044c25f450576d2878e45\n     * the new behavior of the lib breaks it when the lib use the ZXing.DecodeHintType.TRY_HARDER at true\n     *\n     * @override\n     */\n    ZXing.HTMLCanvasElementLuminanceSource.toGrayscaleBuffer = function (\n        imageBuffer,\n        width,\n        height\n    ) {\n        const grayscaleBuffer = new Uint8ClampedArray(width * height);\n        for (let i = 0, j = 0, length = imageBuffer.length; i < length; i += 4, j++) {\n            let gray;\n            const alpha = imageBuffer[i + 3];\n            // The color of fully-transparent pixels is irrelevant. They are often, technically, fully-transparent\n            // black (0 alpha, and then 0 RGB). They are often used, of course as the \"white\" area in a\n            // barcode image. Force any such pixel to be white:\n            if (alpha === 0) {\n                gray = 0xff;\n            } else {\n                const pixelR = imageBuffer[i];\n                const pixelG = imageBuffer[i + 1];\n                const pixelB = imageBuffer[i + 2];\n                // .299R + 0.587G + 0.114B (YUV/YIQ for PAL and NTSC),\n                // (306*R) >> 10 is approximately equal to R*0.299, and so on.\n                // 0x200 >> 10 is 0.5, it implements rounding.\n                gray = (306 * pixelR + 601 * pixelG + 117 * pixelB + 0x200) >> 10;\n            }\n            grayscaleBuffer[j] = gray;\n        }\n        return grayscaleBuffer;\n    };\n\n    /**\n     * ZXingBarcodeDetector class\n     *\n     * BarcodeDetector-like polyfill class using ZXing library.\n     * API follows the Shape Detection Web API (specifically Barcode Detection).\n     */\n    class ZXingBarcodeDetector {\n        /**\n         * @param {object} opts\n         * @param {Array} opts.formats list of codes' formats to detect\n         */\n        constructor(opts = {}) {\n            const formats = opts.formats || allSupportedFormats;\n            const hints = new Map([\n                [\n                    ZXing.DecodeHintType.POSSIBLE_FORMATS,\n                    formats.map((format) => ZXingFormats.get(format)),\n                ],\n                // Enable Scanning at 90 degrees rotation\n                // https://github.com/zxing-js/library/issues/291\n                [ZXing.DecodeHintType.TRY_HARDER, true],\n            ]);\n            this.reader = new ZXing.MultiFormatReader();\n            this.reader.setHints(hints);\n        }\n\n        /**\n         * Detect codes in image.\n         *\n         * @param {HTMLVideoElement} video source video element\n         * @returns {Promise<Array>} array of detected codes\n         */\n        async detect(video) {\n            if (!(video instanceof HTMLVideoElement)) {\n                throw new DOMException(\n                    \"imageDataFrom() requires an HTMLVideoElement\",\n                    \"InvalidArgumentError\"\n                );\n            }\n            if (!isVideoElementReady(video)) {\n                throw new DOMException(\"HTMLVideoElement is not ready\", \"InvalidStateError\");\n            }\n            const canvas = document.createElement(\"canvas\");\n\n            let barcodeArea;\n            if (this.cropArea && (this.cropArea.x || this.cropArea.y)) {\n                barcodeArea = this.cropArea;\n            } else {\n                barcodeArea = {\n                    x: 0,\n                    y: 0,\n                    width: video.videoWidth,\n                    height: video.videoHeight,\n                };\n            }\n            canvas.width = barcodeArea.width;\n            canvas.height = barcodeArea.height;\n\n            const ctx = canvas.getContext(\"2d\");\n\n            ctx.drawImage(\n                video,\n                barcodeArea.x,\n                barcodeArea.y,\n                barcodeArea.width,\n                barcodeArea.height,\n                0,\n                0,\n                barcodeArea.width,\n                barcodeArea.height\n            );\n\n            const luminanceSource = new ZXing.HTMLCanvasElementLuminanceSource(canvas);\n            const binaryBitmap = new ZXing.BinaryBitmap(new ZXing.HybridBinarizer(luminanceSource));\n            try {\n                const result = this.reader.decodeWithState(binaryBitmap);\n                const { resultPoints } = result;\n                const boundingBox = DOMRectReadOnly.fromRect({\n                    x: resultPoints[0].x,\n                    y: resultPoints[0].y,\n                    height: Math.max(1, Math.abs(resultPoints[1].y - resultPoints[0].y)),\n                    width: Math.max(1, Math.abs(resultPoints[1].x - resultPoints[0].x)),\n                });\n                const cornerPoints = resultPoints;\n                const format = Array.from(ZXingFormats).find(\n                    ([k, val]) => val === result.getBarcodeFormat()\n                );\n                const rawValue = result.getText();\n                return [\n                    {\n                        boundingBox,\n                        cornerPoints,\n                        format,\n                        rawValue,\n                    },\n                ];\n            } catch (err) {\n                if (err.name === \"NotFoundException\") {\n                    return [];\n                }\n                throw err;\n            }\n        }\n\n        setCropArea(cropArea) {\n            this.cropArea = cropArea;\n        }\n    }\n\n    /**\n     * Supported codes formats\n     *\n     * @static\n     * @returns {Promise<string[]>}\n     */\n    ZXingBarcodeDetector.getSupportedFormats = async () => allSupportedFormats;\n\n    return ZXingBarcodeDetector;\n}\n\n/**\n * Check for HTMLVideoElement readiness.\n *\n * See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState\n */\nconst HAVE_NOTHING = 0;\nconst HAVE_METADATA = 1;\nexport function isVideoElementReady(video) {\n    return ![HAVE_NOTHING, HAVE_METADATA].includes(video.readyState);\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { BarcodeVideoScanner, isBarcodeScannerSupported } from \"./barcode_video_scanner\";\n\nexport class BarcodeDialog extends Component {\n    static template = \"web.BarcodeDialog\";\n    static components = {\n        BarcodeVideoScanner,\n        Dialog,\n    };\n    static props = [\"facingMode\", \"close\", \"onResult\", \"onError\"];\n\n    setup() {\n        this.state = useState({\n            barcodeScannerSupported: isBarcodeScannerSupported(),\n            errorMessage: _t(\"Check your browser permissions\"),\n        });\n    }\n\n    /**\n     * Detection success handler\n     *\n     * @param {string} result found code\n     */\n    onResult(result) {\n        this.props.close();\n        this.props.onResult(result);\n    }\n\n    /**\n     * Detection error handler\n     *\n     * @param {Error} error\n     */\n    onError(error) {\n        this.state.barcodeScannerSupported = false;\n        this.state.errorMessage = error.message;\n    }\n}\n\n/**\n * Opens the BarcodeScanning dialog and begins code detection using the device's camera.\n *\n * @returns {Promise<string>} resolves when a {qr,bar}code has been detected\n */\nexport async function scanBarcode(env, facingMode = \"environment\") {\n    let res;\n    let rej;\n    const promise = new Promise((resolve, reject) => {\n        res = resolve;\n        rej = reject;\n    });\n    env.services.dialog.add(BarcodeDialog, {\n        facingMode,\n        onResult: (result) => res(result),\n        onError: (error) => rej(error),\n    });\n    return promise;\n}\n", "/* global BarcodeDetector */\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { delay } from \"@web/core/utils/concurrency\";\nimport { loadJS } from \"@web/core/assets\";\nimport { isVideoElementReady, buildZXingBarcodeDetector } from \"./ZXingBarcodeDetector\";\nimport { CropOverlay } from \"./crop_overlay\";\nimport { Component, onMounted, onWillStart, onWillUnmount, useRef, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { pick } from \"@web/core/utils/objects\";\n\nexport class BarcodeVideoScanner extends Component {\n    static template = \"web.BarcodeVideoScanner\";\n    static components = {\n        CropOverlay,\n    };\n    static props = {\n        cssClass: { type: String, optional: true },\n        facingMode: {\n            type: String,\n            validate: (fm) => [\"environment\", \"left\", \"right\", \"user\"].includes(fm),\n        },\n        close: { type: Function, optional: true },\n        onReady: { type: Function, optional: true },\n        onResult: Function,\n        onError: Function,\n        delayBetweenScan: { type: Number, optional: true },\n    };\n    static defaultProps = {\n        cssClass: \"w-100 h-100\",\n    };\n    /**\n     * @override\n     */\n    setup() {\n        this.videoPreviewRef = useRef(\"videoPreview\");\n        this.detectorTimeout = null;\n        this.stream = null;\n        this.detector = null;\n        this.overlayInfo = {};\n        this.zoomRatio = 1;\n        this.scanPaused = false;\n        this.state = useState({\n            isReady: false,\n        });\n\n        onWillStart(async () => {\n            let DetectorClass;\n            // Use Barcode Detection API if available.\n            // As support is still bleeding edge (mainly Chrome on Android),\n            // also provides a fallback using ZXing library.\n            if (\"BarcodeDetector\" in window) {\n                DetectorClass = BarcodeDetector;\n            } else {\n                await loadJS(\"/web/static/lib/zxing-library/zxing-library.js\");\n                DetectorClass = buildZXingBarcodeDetector(window.ZXing);\n            }\n            const formats = await DetectorClass.getSupportedFormats();\n            this.detector = new DetectorClass({ formats });\n        });\n\n        onMounted(async () => {\n            const constraints = {\n                video: { facingMode: this.props.facingMode },\n                audio: false,\n            };\n\n            try {\n                this.stream = await browser.navigator.mediaDevices.getUserMedia(constraints);\n            } catch (err) {\n                const errors = {\n                    NotFoundError: _t(\"No device can be found.\"),\n                    NotAllowedError: _t(\"Odoo needs your authorization first.\"),\n                };\n                const errorMessage = _t(\"Could not start scanning. %(message)s\", {\n                    message: errors[err.name] || err.message,\n                });\n                this.props.onError(new Error(errorMessage));\n                return;\n            }\n            if (!this.videoPreviewRef.el) {\n                this.cleanStreamAndTimeout();\n                const errorMessage = _t(\"Barcode Video Scanner could not be mounted properly.\");\n                this.props.onError(new Error(errorMessage));\n                return;\n            }\n            this.videoPreviewRef.el.srcObject = this.stream;\n            await this.isVideoReady();\n            const { height, width } = getComputedStyle(this.videoPreviewRef.el);\n            const divWidth = width.slice(0, -2);\n            const divHeight = height.slice(0, -2);\n            const tracks = this.stream.getVideoTracks();\n            if (tracks.length) {\n                const [track] = tracks;\n                const settings = track.getSettings();\n                this.zoomRatio = Math.min(divWidth / settings.width, divHeight / settings.height);\n            }\n            this.detectorTimeout = setTimeout(this.detectCode.bind(this), 100);\n        });\n\n        onWillUnmount(() => this.cleanStreamAndTimeout());\n    }\n\n    cleanStreamAndTimeout() {\n        clearTimeout(this.detectorTimeout);\n        this.detectorTimeout = null;\n        if (this.stream) {\n            this.stream.getTracks().forEach((track) => track.stop());\n            this.stream = null;\n        }\n    }\n\n    isZXingBarcodeDetector() {\n        return this.detector && this.detector.__proto__.constructor.name === \"ZXingBarcodeDetector\";\n    }\n\n    /**\n     * Check for camera preview element readiness\n     *\n     * @returns {Promise} resolves when the video element is ready\n     */\n    async isVideoReady() {\n        // FIXME: even if it shouldn't happened, a timeout could be useful here.\n        while (!isVideoElementReady(this.videoPreviewRef.el)) {\n            await delay(10);\n        }\n        this.state.isReady = true;\n        if (this.props.onReady) {\n            this.props.onReady();\n        }\n    }\n\n    onResize(overlayInfo) {\n        this.overlayInfo = overlayInfo;\n        if (this.isZXingBarcodeDetector()) {\n            // TODO need refactoring when ZXing will support multiple result in one scan\n            // https://github.com/zxing-js/library/issues/346\n            this.detector.setCropArea(this.adaptValuesWithRatio(this.overlayInfo, true));\n        }\n    }\n\n    /**\n     * Attempt to detect codes in the current camera preview's frame\n     */\n    async detectCode() {\n        let barcodeDetected = false;\n        let codes = [];\n        try {\n            codes = await this.detector.detect(this.videoPreviewRef.el);\n        } catch (err) {\n            this.props.onError(err);\n        }\n        for (const code of codes) {\n            if (\n                !this.isZXingBarcodeDetector() &&\n                this.overlayInfo.x !== undefined &&\n                this.overlayInfo.y !== undefined\n            ) {\n                const { x, y, width, height } = this.adaptValuesWithRatio(code.boundingBox);\n                if (\n                    x < this.overlayInfo.x ||\n                    x + width > this.overlayInfo.x + this.overlayInfo.width ||\n                    y < this.overlayInfo.y ||\n                    y + height > this.overlayInfo.y + this.overlayInfo.height\n                ) {\n                    continue;\n                }\n            }\n            barcodeDetected = true;\n            this.barcodeDetected(code.rawValue);\n            break;\n        }\n        if (this.stream && (!barcodeDetected || !this.props.delayBetweenScan)) {\n            this.detectorTimeout = setTimeout(this.detectCode.bind(this), 100);\n        }\n    }\n\n    barcodeDetected(barcode) {\n        if (this.props.delayBetweenScan && !this.scanPaused) {\n            this.scanPaused = true;\n            this.detectorTimeout = setTimeout(() => {\n                this.scanPaused = false;\n                this.detectorTimeout = setTimeout(this.detectCode.bind(this), 100);\n            }, this.props.delayBetweenScan);\n        }\n        this.props.onResult(barcode);\n    }\n\n    adaptValuesWithRatio(domRect, dividerRatio = false) {\n        const newObject = pick(domRect, \"x\", \"y\", \"width\", \"height\");\n        for (const key of Object.keys(newObject)) {\n            if (dividerRatio) {\n                newObject[key] /= this.zoomRatio;\n            } else {\n                newObject[key] *= this.zoomRatio;\n            }\n        }\n        return newObject;\n    }\n}\n\n/**\n * Check for BarcodeScanner support\n * @returns {boolean}\n */\nexport function isBarcodeScannerSupported() {\n    return Boolean(browser.navigator.mediaDevices && browser.navigator.mediaDevices.getUserMedia);\n}\n", "import { Component, useRef, onPatched } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { clamp } from \"@web/core/utils/numbers\";\n\nexport class CropOverlay extends Component {\n    static template = \"web.CropOverlay\";\n    static props = {\n        onResize: Function,\n        isReady: Boolean,\n        slots: {\n            type: Object,\n            shape: {\n                default: {},\n            },\n        },\n    };\n\n    setup() {\n        this.localStorageKey = \"o-barcode-scanner-overlay\";\n        this.cropContainerRef = useRef(\"crop-container\");\n        this.isMoving = false;\n        this.boundaryOverlay = {};\n        this.relativePosition = {\n            x: 0,\n            y: 0,\n        };\n        onPatched(() => {\n            this.setupCropRect();\n        });\n    }\n\n    setupCropRect() {\n        if (!this.props.isReady) {\n            return;\n        }\n        this.computeDefaultPoint();\n        this.computeOverlayPosition();\n        this.calculateAndSetTransparentRect();\n        this.executeOnResizeCallback();\n    }\n\n    boundPoint(pointValue, boundaryRect) {\n        return {\n            x: clamp(pointValue.x, boundaryRect.left, boundaryRect.left + boundaryRect.width),\n            y: clamp(pointValue.y, boundaryRect.top, boundaryRect.top + boundaryRect.height),\n        };\n    }\n\n    calculateAndSetTransparentRect() {\n        const cropTransparentRect = this.getTransparentRec(\n            this.relativePosition,\n            this.boundaryOverlay\n        );\n        this.setCropValue(cropTransparentRect, this.relativePosition);\n    }\n\n    computeOverlayPosition() {\n        const cropOverlayElement = this.cropContainerRef.el.querySelector(\".o_crop_overlay\");\n        this.boundaryOverlay = cropOverlayElement.getBoundingClientRect();\n    }\n\n    executeOnResizeCallback() {\n        const transparentRec = this.getTransparentRec(this.relativePosition, this.boundaryOverlay);\n        browser.localStorage.setItem(this.localStorageKey, JSON.stringify(transparentRec));\n        this.props.onResize({\n            ...transparentRec,\n            width: this.boundaryOverlay.width - 2 * transparentRec.x,\n            height: this.boundaryOverlay.height - 2 * transparentRec.y,\n        });\n    }\n\n    computeDefaultPoint() {\n        const firstChildComputedStyle = getComputedStyle(this.cropContainerRef.el.firstChild);\n        const elementWidth = firstChildComputedStyle.width.slice(0, -2);\n        const elementHeight = firstChildComputedStyle.height.slice(0, -2);\n\n        const stringSavedPoint = browser.localStorage.getItem(this.localStorageKey);\n        if (stringSavedPoint) {\n            const savedPoint = JSON.parse(stringSavedPoint);\n            this.relativePosition = {\n                x: clamp(savedPoint.x, 0, elementWidth),\n                y: clamp(savedPoint.y, 0, elementHeight),\n            };\n        } else {\n            const stepWidth = elementWidth / 10;\n            const width = stepWidth * 8;\n            const height = width / 4;\n            const startY = elementHeight / 2 - height / 2;\n            this.relativePosition = {\n                x: stepWidth + width,\n                y: startY + height,\n            };\n        }\n    }\n    getTransparentRec(point, rect) {\n        const middleX = rect.width / 2;\n        const middleY = rect.height / 2;\n        const newDeltaX = Math.abs(point.x - middleX);\n        const newDeltaY = Math.abs(point.y - middleY);\n        return {\n            x: middleX - newDeltaX,\n            y: middleY - newDeltaY,\n        };\n    }\n\n    setCropValue(point, iconPoint) {\n        if (!iconPoint) {\n            iconPoint = point;\n        }\n        this.cropContainerRef.el.style.setProperty(\"--o-crop-x\", `${point.x}px`);\n        this.cropContainerRef.el.style.setProperty(\"--o-crop-y\", `${point.y}px`);\n        this.cropContainerRef.el.style.setProperty(\"--o-crop-icon-x\", `${iconPoint.x}px`);\n        this.cropContainerRef.el.style.setProperty(\"--o-crop-icon-y\", `${iconPoint.y}px`);\n    }\n\n    pointerDown(event) {\n        event.preventDefault();\n        if (event.target.matches(\".o_crop_icon\")) {\n            this.computeOverlayPosition();\n            this.isMoving = true;\n        }\n    }\n\n    pointerMove(event) {\n        if (!this.isMoving) {\n            return;\n        }\n        let eventPosition;\n        if (event.touches && event.touches.length) {\n            eventPosition = event.touches[0];\n        } else {\n            eventPosition = event;\n        }\n        const { clientX, clientY } = eventPosition;\n        const restrictedPosition = this.boundPoint(\n            {\n                x: clientX,\n                y: clientY,\n            },\n            this.boundaryOverlay\n        );\n        this.relativePosition = {\n            x: restrictedPosition.x - this.boundaryOverlay.left,\n            y: restrictedPosition.y - this.boundaryOverlay.top,\n        };\n        this.calculateAndSetTransparentRect(this.relativePosition);\n    }\n\n    pointerUp(event) {\n        this.isMoving = false;\n        this.executeOnResizeCallback();\n    }\n}\n", "/**\n * Browser\n *\n * This file exports an object containing common browser API. It may not look\n * incredibly useful, but it is very convenient when one needs to test code using\n * these methods. With this indirection, it is possible to patch the browser\n * object for a test.\n */\n\nlet sessionStorage = window.sessionStorage;\nlet localStorage = window.localStorage;\ntry {\n    // Safari crashes in Private Browsing\n    localStorage.setItem(\"__localStorage__\", \"true\");\n    localStorage.removeItem(\"__localStorage__\");\n} catch {\n    localStorage = makeRAMLocalStorage();\n    sessionStorage = makeRAMLocalStorage();\n}\n\nexport const browser = {\n    addEventListener: window.addEventListener.bind(window),\n    dispatchEvent: window.dispatchEvent.bind(window),\n    AnalyserNode: window.AnalyserNode,\n    Audio: window.Audio,\n    AudioBufferSourceNode: window.AudioBufferSourceNode,\n    AudioContext: window.AudioContext,\n    AudioWorkletNode: window.AudioWorkletNode,\n    BeforeInstallPromptEvent: window.BeforeInstallPromptEvent?.bind(window),\n    GainNode: window.GainNode,\n    MediaStreamAudioSourceNode: window.MediaStreamAudioSourceNode,\n    removeEventListener: window.removeEventListener.bind(window),\n    setTimeout: window.setTimeout.bind(window),\n    clearTimeout: window.clearTimeout.bind(window),\n    setInterval: window.setInterval.bind(window),\n    clearInterval: window.clearInterval.bind(window),\n    performance: window.performance,\n    requestAnimationFrame: window.requestAnimationFrame.bind(window),\n    cancelAnimationFrame: window.cancelAnimationFrame.bind(window),\n    console: window.console,\n    history: window.history,\n    matchMedia: window.matchMedia.bind(window),\n    navigator,\n    Notification: window.Notification,\n    open: window.open.bind(window),\n    SharedWorker: window.SharedWorker,\n    Worker: window.Worker,\n    XMLHttpRequest: window.XMLHttpRequest,\n    localStorage,\n    sessionStorage,\n    fetch: window.fetch.bind(window),\n    innerHeight: window.innerHeight,\n    innerWidth: window.innerWidth,\n    ontouchstart: window.ontouchstart,\n    BroadcastChannel: window.BroadcastChannel,\n};\n\nObject.defineProperty(browser, \"location\", {\n    set(val) {\n        window.location = val;\n    },\n    get() {\n        return window.location;\n    },\n    configurable: true,\n});\n\nObject.defineProperty(browser, \"innerHeight\", {\n    get: () => window.innerHeight,\n    configurable: true,\n});\nObject.defineProperty(browser, \"innerWidth\", {\n    get: () => window.innerWidth,\n    configurable: true,\n});\n\n// -----------------------------------------------------------------------------\n// memory localStorage\n// -----------------------------------------------------------------------------\n\n/**\n * @returns {typeof window[\"localStorage\"]}\n */\nexport function makeRAMLocalStorage() {\n    let store = {};\n    return {\n        setItem(key, value) {\n            const newValue = String(value);\n            store[key] = newValue;\n            window.dispatchEvent(new StorageEvent(\"storage\", { key, newValue }));\n        },\n        getItem(key) {\n            return store[key] ?? null;\n        },\n        clear() {\n            store = {};\n        },\n        removeItem(key) {\n            delete store[key];\n            window.dispatchEvent(new StorageEvent(\"storage\", { key, newValue: null }));\n        },\n        get length() {\n            return Object.keys(store).length;\n        },\n        key() {\n            return \"\";\n        },\n    };\n}\n", "import { browser } from \"./browser\";\n\n// -----------------------------------------------------------------------------\n// Feature detection\n// -----------------------------------------------------------------------------\n\n/**\n * True if the browser is based on Chromium (Google Chrome, Opera, Edge).\n */\nexport function isBrowserChrome() {\n    return /Chrome/i.test(browser.navigator.userAgent);\n}\n\nexport function isBrowserFirefox() {\n    return /Firefox/i.test(browser.navigator.userAgent);\n}\n\n/**\n * true if the browser is based on Safari (Safari, Epiphany)\n *\n * @returns {boolean}\n */\nexport function isBrowserSafari() {\n    return !isBrowserChrome() && browser.navigator.userAgent.includes(\"Safari\");\n}\n\nexport function isAndroid() {\n    return /Android/i.test(browser.navigator.userAgent);\n}\n\nexport function isIOS() {\n    return (\n        /(iPad|iPhone|iPod)/i.test(browser.navigator.userAgent) ||\n        (browser.navigator.platform === \"MacIntel\" && maxTouchPoints() > 1)\n    );\n}\n\nexport function isOtherMobileOS() {\n    return /(webOS|BlackBerry|Windows Phone)/i.test(browser.navigator.userAgent);\n}\n\nexport function isMacOS() {\n    return /Mac/i.test(browser.navigator.userAgent);\n}\n\nexport function isMobileOS() {\n    return isAndroid() || isIOS() || isOtherMobileOS();\n}\n\nexport function isIosApp() {\n    return /OdooMobile \\(iOS\\)/i.test(browser.navigator.userAgent);\n}\n\nexport function isAndroidApp() {\n    return /OdooMobile.+Android/i.test(browser.navigator.userAgent);\n}\n\nexport function isDisplayStandalone() {\n    return browser.matchMedia(\"(display-mode: standalone)\").matches;\n}\n\nexport function hasTouch() {\n    return browser.ontouchstart !== undefined || browser.matchMedia(\"(pointer:coarse)\").matches;\n}\n\nexport function maxTouchPoints() {\n    return browser.navigator.maxTouchPoints || 1;\n}\n", "import { EventBus } from \"@odoo/owl\";\nimport { omit, pick } from \"../utils/objects\";\nimport { compareUrls, objectToUrlEncodedString } from \"../utils/urls\";\nimport { browser } from \"./browser\";\nimport { isDisplayStandalone } from \"@web/core/browser/feature_detection\";\nimport { slidingWindow } from \"@web/core/utils/arrays\";\nimport { isNumeric } from \"@web/core/utils/strings\";\n\n// Keys that are serialized in the URL as path segments instead of query string\nexport const PATH_KEYS = [\"resId\", \"action\", \"active_id\", \"model\"];\n\nexport const routerBus = new EventBus();\n\nfunction isScopedApp() {\n    return browser.location.href.includes(\"/scoped_app\") && isDisplayStandalone();\n}\n\n/**\n * Casts the given string to a number if possible.\n *\n * @param {string} value\n * @returns {string|number}\n */\nfunction cast(value) {\n    return !value || isNaN(value) ? value : Number(value);\n}\n\n/**\n * @typedef {{ [key: string]: string }} Query\n * @typedef {{ [key: string]: any }} Route\n */\n\nfunction parseString(str) {\n    const parts = str.split(\"&\");\n    const result = {};\n    for (const part of parts) {\n        const [key, value] = part.split(\"=\");\n        const decoded = decodeURIComponent(value || \"\");\n        result[key] = cast(decoded);\n    }\n    return result;\n}\n/**\n * @param {object} values An object with the values of the new state\n * @param {boolean} replace whether the values should replace the state or be\n *  layered on top of the current state\n * @returns {object} the next state of the router\n */\nfunction computeNextState(values, replace) {\n    const nextState = replace ? pick(state, ..._lockedKeys) : { ...state };\n    Object.assign(nextState, values);\n    // Update last entry in the actionStack\n    if (nextState.actionStack?.length) {\n        Object.assign(nextState.actionStack.at(-1), pick(nextState, ...PATH_KEYS));\n    }\n    return sanitizeSearch(nextState);\n}\n\nfunction sanitize(obj, valueToRemove) {\n    return Object.fromEntries(\n        Object.entries(obj)\n            .filter(([, v]) => v !== valueToRemove)\n            .map(([k, v]) => [k, cast(v)])\n    );\n}\n\nfunction sanitizeSearch(search) {\n    return sanitize(search);\n}\n\nfunction sanitizeHash(hash) {\n    return sanitize(hash, \"\");\n}\n\n/**\n * @param {string} hash\n * @returns {any}\n */\nexport function parseHash(hash) {\n    return hash && hash !== \"#\" ? parseString(hash.slice(1)) : {};\n}\n\n/**\n * @param {string} search\n * @returns {any}\n */\nexport function parseSearchQuery(search) {\n    return search ? parseString(search.slice(1)) : {};\n}\n\nfunction pathFromActionState(state) {\n    const path = [];\n    const { action, model, active_id, resId } = state;\n    if (active_id && typeof active_id === \"number\") {\n        path.push(active_id);\n    }\n    if (action) {\n        if (typeof action === \"number\" || action.includes(\".\")) {\n            path.push(`action-${action}`);\n        } else {\n            path.push(action);\n        }\n    } else if (model) {\n        if (model.includes(\".\")) {\n            path.push(model);\n        } else {\n            // A few models don't have a dot at all, we need to distinguish\n            // them from action paths (eg: website)\n            path.push(`m-${model}`);\n        }\n    }\n    if (resId && (typeof resId === \"number\" || resId === \"new\")) {\n        path.push(resId);\n    }\n    return path.join(\"/\");\n}\n\n/**\n * @param {{ [key: string]: any }} state\n * @returns\n */\nexport function stateToUrl(state) {\n    let path = \"\";\n    const pathKeysToOmit = [..._hiddenKeysFromUrl];\n    const actionStack = (state.actionStack || [state]).map((a) => ({ ...a }));\n    if (actionStack.at(-1)?.action !== \"menu\") {\n        for (const [prevAct, currentAct] of slidingWindow(actionStack, 2).reverse()) {\n            const { action: prevAction, resId: prevResId, active_id: prevActiveId } = prevAct;\n            const { action: currentAction, active_id: currentActiveId } = currentAct;\n            // actions would typically map to a path like `active_id/action/res_id`\n            if (currentActiveId === prevResId) {\n                // avoid doubling up when the active_id is the same as the previous action's res_id\n                delete currentAct.active_id;\n            }\n            if (prevAction === currentAction && !prevResId && currentActiveId === prevActiveId) {\n                //avoid doubling up the action and the active_id when a single-record action is preceded by a multi-record action\n                delete currentAct.action;\n                delete currentAct.active_id;\n            }\n        }\n        const pathSegments = actionStack.map(pathFromActionState).filter(Boolean);\n        if (pathSegments.length) {\n            path = `/${pathSegments.join(\"/\")}`;\n        }\n    }\n    if (state.active_id && typeof state.active_id !== \"number\") {\n        pathKeysToOmit.splice(pathKeysToOmit.indexOf(\"active_id\"), 1);\n    }\n    if (state.resId && typeof state.resId !== \"number\" && state.resId !== \"new\") {\n        pathKeysToOmit.splice(pathKeysToOmit.indexOf(\"resId\"), 1);\n    }\n    const search = objectToUrlEncodedString(omit(state, ...pathKeysToOmit));\n    const start_url = isScopedApp() ? \"scoped_app\" : \"odoo\";\n    return `/${start_url}${path}${search ? `?${search}` : \"\"}`;\n}\n\nexport function urlToState(urlObj) {\n    const { pathname, hash, search } = urlObj;\n    const state = parseSearchQuery(search);\n\n    // ** url-retrocompatibility **\n    // If the url contains a hash, it can be for two motives:\n    // 1. It is an anchor link, in that case, we ignore it, as it will not have a keys/values format\n    //    the sanitizeHash function will remove it from the hash object.\n    // 2. It has one or more keys/values, in that case, we merge it with the search.\n    if (pathname === \"/web\") {\n        const sanitizedHash = sanitizeHash(parseHash(hash));\n        // Old urls used \"id\", it is now resId for clarity. Remap to the new name.\n        if (sanitizedHash.id) {\n            sanitizedHash.resId = sanitizedHash.id;\n            delete sanitizedHash.id;\n            delete sanitizedHash.view_type;\n        } else if (sanitizedHash.view_type === \"form\") {\n            sanitizedHash.resId = \"new\";\n            delete sanitizedHash.view_type;\n        }\n        Object.assign(state, sanitizedHash);\n        const url = browser.location.origin + router.stateToUrl(state);\n        urlObj.href = url;\n    }\n\n    const [prefix, ...splitPath] = urlObj.pathname.split(\"/\").filter(Boolean);\n\n    if (prefix === \"odoo\" || isScopedApp()) {\n        const actionParts = [...splitPath.entries()].filter(\n            ([_, part]) => !isNumeric(part) && part !== \"new\"\n        );\n        const actions = [];\n        for (const [i, part] of actionParts) {\n            const action = {};\n            const [left, right] = [splitPath[i - 1], splitPath[i + 1]];\n            if (isNumeric(left)) {\n                action.active_id = parseInt(left);\n            }\n\n            if (right === \"new\") {\n                action.resId = \"new\";\n            } else if (isNumeric(right)) {\n                action.resId = parseInt(right);\n            }\n\n            if (part.startsWith(\"action-\")) {\n                // numeric id or xml_id\n                const actionId = part.slice(7);\n                action.action = isNumeric(actionId) ? parseInt(actionId) : actionId;\n            } else if (part.startsWith(\"m-\")) {\n                action.model = part.slice(2);\n            } else if (part.includes(\".\")) {\n                action.model = part;\n            } else {\n                // action tag or path\n                action.action = part;\n            }\n\n            if (action.resId && action.action) {\n                actions.push(omit(action, \"resId\"));\n            }\n            // Don't create actions for models without resId unless they're the last one.\n            // If the last one is a model but doesn't have a view_type, the action service will not mount it anyway.\n            if (action.action || action.resId || i === splitPath.length - 1) {\n                actions.push(action);\n            }\n        }\n        const activeAction = actions.at(-1);\n        if (activeAction) {\n            Object.assign(state, activeAction);\n            state.actionStack = actions;\n        }\n    }\n    return state;\n}\n\nlet state;\nlet pushTimeout;\nlet pushArgs;\nlet _lockedKeys;\nlet _hiddenKeysFromUrl = new Set();\n\nexport function startRouter() {\n    const url = new URL(browser.location);\n    state = router.urlToState(url);\n    // ** url-retrocompatibility **\n    if (browser.location.pathname === \"/web\") {\n        // Change the url of the current history entry to the canonical url.\n        // This change should be done only at the first load, and not when clicking on old style internal urls.\n        // Or when clicking back/forward on the browser.\n        browser.history.replaceState(browser.history.state, null, url.href);\n    }\n    pushTimeout = null;\n    pushArgs = {\n        replace: false,\n        reload: false,\n        state: {},\n    };\n    _lockedKeys = new Set([\"debug\", \"lang\"]);\n    _hiddenKeysFromUrl = new Set([...PATH_KEYS, \"actionStack\"]);\n}\n\n/**\n * When the user navigates history using the back/forward button, the browser\n * dispatches a popstate event with the state that was in the history for the\n * corresponding history entry. We just adopt that state so that the webclient\n * can use that previous state without forcing a full page reload.\n */\nbrowser.addEventListener(\"popstate\", (ev) => {\n    browser.clearTimeout(pushTimeout);\n    if (!ev.state) {\n        // We are coming from a click on an anchor.\n        // Add the current state to the history entry so that a future loadstate behaves as expected.\n        browser.history.replaceState({ nextState: state }, \"\", browser.location.href);\n        return;\n    }\n    state = ev.state?.nextState || router.urlToState(new URL(browser.location));\n    // Some client actions want to handle loading their own state. This is a ugly hack to allow not\n    // reloading the webclient's state when they manipulate history.\n    if (!ev.state?.skipRouteChange && !router.skipLoad) {\n        routerBus.trigger(\"ROUTE_CHANGE\");\n    }\n    router.skipLoad = false;\n});\n\n/**\n * When clicking internal links, do a loadState instead of a full page reload.\n * This also alows the mobile app to not open an in-app browser for them.\n */\nbrowser.addEventListener(\"click\", (ev) => {\n    if (ev.defaultPrevented || ev.target.closest(\"[contenteditable]\")) {\n        return;\n    }\n    const href = ev.target.closest(\"a\")?.getAttribute(\"href\");\n    if (href && !href.startsWith(\"#\")) {\n        let url;\n        try {\n            // ev.target.href is the full url including current path\n            url = new URL(ev.target.closest(\"a\").href);\n        } catch {\n            return;\n        }\n        if (\n            browser.location.host === url.host &&\n            browser.location.pathname.startsWith(\"/odoo\") &&\n            ([\"/web\", \"/odoo\"].includes(url.pathname) || url.pathname.startsWith(\"/odoo/\")) &&\n            ev.target.target !== \"_blank\"\n        ) {\n            ev.preventDefault();\n            state = router.urlToState(url);\n            if (url.pathname.startsWith(\"/odoo\") && url.hash) {\n                browser.history.pushState({}, \"\", url.href);\n            }\n            new Promise((res) => setTimeout(res, 0)).then(() => routerBus.trigger(\"ROUTE_CHANGE\"));\n        }\n    }\n});\n\n/**\n * @param {string} mode\n */\nfunction makeDebouncedPush(mode) {\n    function doPush() {\n        // Calculates new route based on aggregated search and options\n        const nextState = computeNextState(pushArgs.state, pushArgs.replace);\n        const url = browser.location.origin + router.stateToUrl(nextState);\n        if (!compareUrls(url + browser.location.hash, browser.location.href)) {\n            // If the route changed: pushes or replaces browser state\n            if (mode === \"push\") {\n                // Because doPush is delayed, the history entry will have the wrong name.\n                // We set the document title to what it was at the time of the pushState\n                // call, then push, which generates the history entry with the right title\n                // then restore the title to what it's supposed to be\n                const originalTitle = document.title;\n                document.title = pushArgs.title;\n                browser.history.pushState({ nextState }, \"\", url);\n                document.title = originalTitle;\n            } else {\n                browser.history.replaceState({ nextState }, \"\", url);\n            }\n        } else {\n            // URL didn't change but state might have, update it in place\n            browser.history.replaceState({ nextState }, \"\", browser.location.href);\n        }\n        state = nextState;\n        if (pushArgs.reload) {\n            browser.location.reload();\n        }\n    }\n    /**\n     * @param {object} state\n     * @param {object} options\n     */\n    return function pushOrReplaceState(state, options = {}) {\n        pushArgs.replace ||= options.replace;\n        pushArgs.reload ||= options.reload;\n        pushArgs.title = document.title;\n        Object.assign(pushArgs.state, state);\n        browser.clearTimeout(pushTimeout);\n        const push = () => {\n            doPush();\n            pushTimeout = null;\n            pushArgs = {\n                replace: false,\n                reload: false,\n                state: {},\n            };\n        };\n        if (options.sync) {\n            push();\n        } else {\n            pushTimeout = browser.setTimeout(() => {\n                push();\n            });\n        }\n    };\n}\n\nexport const router = {\n    get current() {\n        return state;\n    },\n    // state <-> url conversions can be patched if needed in a custom webclient.\n    stateToUrl,\n    urlToState,\n    // TODO: stop debouncing these and remove the ugly hack to have the correct title for history entries\n    pushState: makeDebouncedPush(\"push\"),\n    replaceState: makeDebouncedPush(\"replace\"),\n    cancelPushes: () => browser.clearTimeout(pushTimeout),\n    addLockedKey: (key) => _lockedKeys.add(key),\n    hideKeyFromUrl: (key) => _hiddenKeysFromUrl.add(key),\n    skipLoad: false,\n};\n\nstartRouter();\n\nexport function objectToQuery(obj) {\n    const query = {};\n    Object.entries(obj).forEach(([k, v]) => {\n        query[k] = v ? String(v) : v;\n    });\n    return query;\n}\n", "import { useHotkey } from \"../hotkeys/hotkey_hook\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\n/**\n * Custom checkbox\n *\n * <CheckBox\n *    value=\"boolean\"\n *    disabled=\"boolean\"\n *    onChange=\"_onValueChange\"\n * >\n *    Change the label text\n * </CheckBox>\n *\n * @extends Component\n */\n\nexport class CheckBox extends Component {\n    static template = \"web.CheckBox\";\n    static nextId = 1;\n    static defaultProps = {\n        onChange: () => {},\n    };\n    static props = {\n        id: {\n            type: true,\n            optional: true,\n        },\n        disabled: {\n            type: Boolean,\n            optional: true,\n        },\n        value: {\n            type: Boolean,\n            optional: true,\n        },\n        slots: {\n            type: Object,\n            optional: true,\n        },\n        onChange: {\n            type: Function,\n            optional: true,\n        },\n        className: {\n            type: String,\n            optional: true,\n        },\n        name: {\n            type: String,\n            optional: true,\n        },\n    };\n\n    setup() {\n        this.id = `checkbox-comp-${CheckBox.nextId++}`;\n        this.rootRef = useRef(\"root\");\n\n        // Make it toggleable through the Enter hotkey\n        // when the focus is inside the root element\n        useHotkey(\n            \"Enter\",\n            ({ area }) => {\n                const oldValue = area.querySelector(\"input\").checked;\n                this.props.onChange(!oldValue);\n            },\n            { area: () => this.rootRef.el, bypassEditableProtection: true }\n        );\n    }\n\n    onClick(ev) {\n        if (ev.composedPath().find((el) => [\"INPUT\", \"LABEL\"].includes(el.tagName))) {\n            // The onChange will handle these cases.\n            ev.stopPropagation();\n            return;\n        }\n\n        // Reproduce the click event behavior as if it comes from the input element.\n        const input = this.rootRef.el.querySelector(\"input\");\n        input.focus();\n        if (!this.props.disabled) {\n            ev.stopPropagation();\n            input.checked = !input.checked;\n            this.props.onChange(input.checked);\n        }\n    }\n\n    onChange(ev) {\n        if (!this.props.disabled) {\n            this.props.onChange(ev.target.checked);\n        }\n    }\n}\n", "import { Component, onWillDestroy, onWillStart, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { useDebounced } from \"@web/core/utils/timing\";\n\nfunction onResized(ref, callback) {\n    const _ref = typeof ref === \"string\" ? useRef(ref) : ref;\n    const resizeObserver = new ResizeObserver(callback);\n\n    useEffect(\n        (el) => {\n            if (el) {\n                resizeObserver.observe(el);\n                return () => resizeObserver.unobserve(el);\n            }\n        },\n        () => [_ref.el]\n    );\n\n    onWillDestroy(() => {\n        resizeObserver.disconnect();\n    });\n}\n\nexport class CodeEditor extends Component {\n    static template = \"web.CodeEditor\";\n    static components = {};\n    static props = {\n        mode: {\n            type: String,\n            optional: true,\n            validate: (mode) => CodeEditor.MODES.includes(mode),\n        },\n        value: { validate: (v) => typeof v === \"string\", optional: true },\n        readonly: { type: Boolean, optional: true },\n        onChange: { type: Function, optional: true },\n        onBlur: { type: Function, optional: true },\n        class: { type: String, optional: true },\n        theme: {\n            type: String,\n            optional: true,\n            validate: (theme) => CodeEditor.THEMES.includes(theme),\n        },\n        maxLines: { type: Number, optional: true },\n        sessionId: { type: [Number, String], optional: true },\n    };\n    static defaultProps = {\n        readonly: false,\n        value: \"\",\n        onChange: () => {},\n        class: \"\",\n        theme: \"\",\n        sessionId: 1,\n    };\n\n    static MODES = [\"javascript\", \"xml\", \"qweb\", \"scss\", \"python\"];\n    static THEMES = [\"\", \"monokai\"];\n\n    setup() {\n        this.editorRef = useRef(\"editorRef\");\n        this.state = useState({\n            activeMode: undefined,\n        });\n\n        onWillStart(async () => await loadBundle(\"web.ace_lib\"));\n\n        const sessions = {};\n        // The ace library triggers the \"change\" event even if the change is\n        // programmatic. Even worse, it triggers 2 \"change\" events in that case,\n        // one with the empty string, and one with the new value. We only want\n        // to notify the parent of changes done by the user, in the UI, so we\n        // use this flag to filter out noisy \"change\" events.\n        let ignoredAceChange = false;\n        useEffect(\n            (el) => {\n                if (!el) {\n                    return;\n                }\n\n                // keep in closure\n                const aceEditor = window.ace.edit(el);\n                this.aceEditor = aceEditor;\n\n                this.aceEditor.setOptions({\n                    maxLines: this.props.maxLines,\n                    showPrintMargin: false,\n                    useWorker: false,\n                });\n                this.aceEditor.$blockScrolling = true;\n\n                this.aceEditor.on(\"changeMode\", () => {\n                    this.state.activeMode = this.aceEditor.getSession().$modeId.split(\"/\").at(-1);\n                });\n\n                const session = aceEditor.getSession();\n                if (!sessions[this.props.sessionId]) {\n                    sessions[this.props.sessionId] = session;\n                }\n                session.setValue(this.props.value);\n                session.on(\"change\", () => {\n                    if (this.props.onChange && !ignoredAceChange) {\n                        this.props.onChange(this.aceEditor.getValue());\n                    }\n                });\n                this.aceEditor.on(\"blur\", () => {\n                    if (this.props.onBlur) {\n                        this.props.onBlur();\n                    }\n                });\n\n                return () => {\n                    aceEditor.destroy();\n                };\n            },\n            () => [this.editorRef.el]\n        );\n\n        useEffect(\n            (theme) => this.aceEditor.setTheme(theme ? `ace/theme/${theme}` : \"\"),\n            () => [this.props.theme]\n        );\n\n        useEffect(\n            (readonly) => {\n                this.aceEditor.setOptions({\n                    readOnly: readonly,\n                    highlightActiveLine: !readonly,\n                    highlightGutterLine: !readonly,\n                });\n\n                this.aceEditor.renderer.setOptions({\n                    displayIndentGuides: !readonly,\n                    showGutter: !readonly,\n                });\n\n                this.aceEditor.renderer.$cursorLayer.element.style.display = readonly\n                    ? \"none\"\n                    : \"block\";\n            },\n            () => [this.props.readonly]\n        );\n\n        useEffect(\n            (sessionId, mode, value) => {\n                let session = sessions[sessionId];\n                if (session) {\n                    if (session.getValue() !== value) {\n                        ignoredAceChange = true;\n                        session.setValue(value);\n                        ignoredAceChange = false;\n                    }\n                } else {\n                    session = new window.ace.EditSession(value);\n                    session.setUndoManager(new window.ace.UndoManager());\n                    session.setOptions({\n                        useWorker: false,\n                        tabSize: 2,\n                        useSoftTabs: true,\n                    });\n                    session.on(\"change\", () => {\n                        if (this.props.onChange && !ignoredAceChange) {\n                            this.props.onChange(this.aceEditor.getValue());\n                        }\n                    });\n                    sessions[sessionId] = session;\n                }\n                session.setMode(mode ? `ace/mode/${mode}` : \"\");\n                this.aceEditor.setSession(session);\n            },\n            () => [this.props.sessionId, this.props.mode, this.props.value]\n        );\n\n        const debouncedResize = useDebounced(() => {\n            if (this.aceEditor) {\n                this.aceEditor.resize();\n            }\n        }, 250);\n\n        onResized(this.editorRef, debouncedResize);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\n\nimport { Component, useRef, useState, useExternalListener } from \"@odoo/owl\";\n\nexport class ColorList extends Component {\n    static COLORS = [\n        _t(\"No color\"),\n        _t(\"Red\"),\n        _t(\"Orange\"),\n        _t(\"Yellow\"),\n        _t(\"Cyan\"),\n        _t(\"Purple\"),\n        _t(\"Almond\"),\n        _t(\"Teal\"),\n        _t(\"Blue\"),\n        _t(\"Raspberry\"),\n        _t(\"Green\"),\n        _t(\"Violet\"),\n    ];\n    static template = \"web.ColorList\";\n    static defaultProps = {\n        forceExpanded: false,\n        isExpanded: false,\n    };\n    static props = {\n        canToggle: { type: Boolean, optional: true },\n        colors: Array,\n        forceExpanded: { type: Boolean, optional: true },\n        isExpanded: { type: Boolean, optional: true },\n        onColorSelected: Function,\n        selectedColor: { type: Number, optional: true },\n    };\n\n    setup() {\n        this.colorlistRef = useRef(\"colorlist\");\n        this.state = useState({ isExpanded: this.props.isExpanded });\n        useExternalListener(window, \"click\", this.onOutsideClick);\n    }\n    get colors() {\n        return this.constructor.COLORS;\n    }\n    onColorSelected(id) {\n        this.props.onColorSelected(id);\n        if (!this.props.forceExpanded) {\n            this.state.isExpanded = false;\n        }\n    }\n    onOutsideClick(ev) {\n        if (this.colorlistRef.el.contains(ev.target) || this.props.forceExpanded) {\n            return;\n        }\n        this.state.isExpanded = false;\n    }\n    onToggle(ev) {\n        if (this.props.canToggle) {\n            ev.preventDefault();\n            ev.stopPropagation();\n            this.state.isExpanded = !this.state.isExpanded;\n            this.colorlistRef.el.firstElementChild.focus();\n        }\n    }\n}\n", "import {\n    convertCSSColorToRgba,\n    convertHslToRgb,\n    convertRgbaToCSSColor,\n    convertRgbToHsl,\n} from \"@web/core/utils/colors\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { clamp } from \"@web/core/utils/numbers\";\nimport { debounce, useThrottleForAnimation } from \"@web/core/utils/timing\";\n\nimport { Component, onMounted, onWillUpdateProps, useExternalListener, useRef } from \"@odoo/owl\";\n\nexport class Colorpicker extends Component {\n    static template = \"web.Colorpicker\";\n    static props = {\n        document: { type: true, optional: true },\n        defaultColor: { type: String, optional: true },\n        selectedColor: { type: String, optional: true },\n        noTransparency: { type: Boolean, optional: true },\n        stopClickPropagation: { type: Boolean, optional: true },\n        onColorSelect: { type: Function, optional: true },\n        onColorPreview: { type: Function, optional: true },\n        onInputEnter: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        document: window.document,\n        defaultColor: \"#FF0000\",\n        noTransparency: false,\n        stopClickPropagation: false,\n        onColorSelect: () => {},\n        onColorPreview: () => {},\n        onInputEnter: () => {},\n    };\n\n    setup() {\n        this.pickerFlag = false;\n        this.sliderFlag = false;\n        this.opacitySliderFlag = false;\n        this.colorComponents = {};\n        this.uniqueId = uniqueId(\"colorpicker\");\n        this.selectedHexValue = \"\";\n\n        this.debouncedOnChangeInputs = debounce(this.onChangeInputs.bind(this), 10, true);\n\n        this.elRef = useRef(\"el\");\n        this.colorPickerAreaRef = useRef(\"colorPickerArea\");\n        this.colorPickerPointerRef = useRef(\"colorPickerPointer\");\n        this.colorSliderRef = useRef(\"colorSlider\");\n        this.colorSliderPointerRef = useRef(\"colorSliderPointer\");\n        this.opacitySliderRef = useRef(\"opacitySlider\");\n        this.opacitySliderPointerRef = useRef(\"opacitySliderPointer\");\n\n        // Need to be bound on all documents to work in all possible cases (we\n        // have to be able to start dragging/moving from the colorpicker to\n        // anywhere on the screen, crossing iframes).\n        const documents = [\n            window.top,\n            ...Array.from(window.top.frames).filter((frame) => {\n                try {\n                    const document = frame.document;\n                    return !!document;\n                } catch {\n                    // We cannot access the document (cross origin).\n                    return false;\n                }\n            }),\n        ].map((w) => w.document);\n        this.throttleOnMouseMove = useThrottleForAnimation((ev) => {\n            this.onMouseMovePicker(ev);\n            this.onMouseMoveSlider(ev);\n            this.onMouseMoveOpacitySlider(ev);\n        });\n\n        for (const doc of documents) {\n            useExternalListener(doc, \"mousemove\", this.throttleOnMouseMove);\n            useExternalListener(doc, \"mouseup\", this.onMouseUp.bind(this));\n        }\n        onMounted(async () => {\n            const rgba = convertCSSColorToRgba(this.props.defaultColor);\n            if (rgba) {\n                this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity);\n            }\n\n            this.previewActive = true;\n            this._updateUI();\n        });\n        onWillUpdateProps((newProps) => {\n            if (newProps.selectedColor) {\n                this.setSelectedColor(newProps.selectedColor);\n            }\n        });\n    }\n\n    /**\n     * Sets the currently selected color\n     *\n     * @param {string} color rgb[a]\n     */\n    setSelectedColor(color) {\n        const rgba = convertCSSColorToRgba(color);\n        if (rgba) {\n            const oldPreviewActive = this.previewActive;\n            this.previewActive = false;\n            this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity);\n            this.previewActive = oldPreviewActive;\n            this._updateUI();\n        }\n    }\n\n    get el() {\n        return this.elRef.el;\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Updates input values, color preview, picker and slider pointer positions.\n     *\n     * @private\n     */\n    _updateUI() {\n        // Update inputs\n        for (const [color, value] of Object.entries(this.colorComponents)) {\n            const input = this.el.querySelector(`.o_${color}_input`);\n            if (input) {\n                input.value = value;\n            }\n        }\n\n        // Update picker area and picker pointer position\n        const colorPickerArea = this.colorPickerAreaRef.el;\n        colorPickerArea.style.backgroundColor = `hsl(${this.colorComponents.hue}, 100%, 50%)`;\n        const top = ((100 - this.colorComponents.lightness) * colorPickerArea.clientHeight) / 100;\n        const left = (this.colorComponents.saturation * colorPickerArea.clientWidth) / 100;\n\n        const colorpickerPointer = this.colorPickerPointerRef.el;\n        colorpickerPointer.style.top = top - 5 + \"px\";\n        colorpickerPointer.style.left = left - 5 + \"px\";\n\n        // Update color slider position\n        const colorSlider = this.colorSliderRef.el;\n        const height = colorSlider.clientHeight;\n        const y = (this.colorComponents.hue * height) / 360;\n        this.colorSliderPointerRef.el.style.top = `${Math.round(y - 2)}px`;\n\n        if (!this.props.noTransparency) {\n            // Update opacity slider position\n            const opacitySlider = this.opacitySliderRef.el;\n            const heightOpacity = opacitySlider.clientHeight;\n            const z = heightOpacity * (1 - this.colorComponents.opacity / 100.0);\n            this.opacitySliderPointerRef.el.style.top = `${Math.round(z - 2)}px`;\n\n            // Add gradient color on opacity slider\n            opacitySlider.style.background = `linear-gradient(${this.colorComponents.hex} 0%, transparent 100%)`;\n        }\n    }\n    /**\n     * Updates colors according to given hex value. Opacity is left unchanged.\n     *\n     * @private\n     * @param {string} hex - hexadecimal code\n     */\n    _updateHex(hex) {\n        const rgb = convertCSSColorToRgba(hex);\n        if (!rgb) {\n            return;\n        }\n        Object.assign(\n            this.colorComponents,\n            { hex: hex },\n            rgb,\n            convertRgbToHsl(rgb.red, rgb.green, rgb.blue)\n        );\n        this._updateCssColor();\n    }\n    /**\n     * Updates colors according to given RGB values.\n     *\n     * @private\n     * @param {integer} r\n     * @param {integer} g\n     * @param {integer} b\n     * @param {integer} [a]\n     */\n    _updateRgba(r, g, b, a) {\n        // Remove full transparency in case some lightness is added\n        const opacity = a || this.colorComponents.opacity;\n        if (opacity < 0.1 && (r > 0.1 || g > 0.1 || b > 0.1)) {\n            a = 100;\n        }\n\n        // We update the hexadecimal code by transforming into a css color and\n        // ignoring the opacity (we don't display opacity component in hexa as\n        // not supported on all browsers)\n        const hex = convertRgbaToCSSColor(r, g, b);\n        if (!hex) {\n            return;\n        }\n        Object.assign(\n            this.colorComponents,\n            { red: r, green: g, blue: b },\n            a === undefined ? {} : { opacity: a },\n            { hex: hex },\n            convertRgbToHsl(r, g, b)\n        );\n        this._updateCssColor();\n    }\n    /**\n     * Updates colors according to given HSL values.\n     *\n     * @private\n     * @param {integer} h\n     * @param {integer} s\n     * @param {integer} l\n     */\n    _updateHsl(h, s, l) {\n        // Remove full transparency in case some lightness is added\n        let a = this.colorComponents.opacity;\n        if (a < 0.1 && l > 0.1) {\n            a = 100;\n        }\n\n        const rgb = convertHslToRgb(h, s, l);\n        if (!rgb) {\n            return;\n        }\n        // We receive an hexa as we ignore the opacity\n        const hex = convertRgbaToCSSColor(rgb.red, rgb.green, rgb.blue);\n        Object.assign(\n            this.colorComponents,\n            { hue: h, saturation: s, lightness: l },\n            rgb,\n            { hex: hex },\n            { opacity: a }\n        );\n        this._updateCssColor();\n    }\n    /**\n     * Updates color opacity.\n     *\n     * @private\n     * @param {integer} a\n     */\n    _updateOpacity(a) {\n        if (a < 0 || a > 100) {\n            return;\n        }\n        Object.assign(this.colorComponents, { opacity: a });\n        this._updateCssColor();\n    }\n    /**\n     * Trigger an event to annonce that the widget value has changed\n     *\n     * @private\n     */\n    _colorSelected() {\n        this.props.onColorSelect(this.colorComponents);\n    }\n    /**\n     * Updates css color representation.\n     *\n     * @private\n     */\n    _updateCssColor() {\n        const r = this.colorComponents.red;\n        const g = this.colorComponents.green;\n        const b = this.colorComponents.blue;\n        const a = this.colorComponents.opacity;\n        Object.assign(this.colorComponents, { cssColor: convertRgbaToCSSColor(r, g, b, a) });\n        if (this.previewActive) {\n            this.props.onColorPreview(this.colorComponents);\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    onKeydown(ev) {\n        if (ev.key === \"Enter\") {\n            if (ev.target.tagName === \"INPUT\") {\n                this.onChangeInputs(ev);\n            }\n            ev.preventDefault();\n            this.props.onInputEnter(ev);\n        }\n    }\n    /**\n     * @param {Event} ev\n     */\n    onClick(ev) {\n        if (this.props.stopClickPropagation) {\n            ev.stopPropagation();\n        }\n        //TODO: we should remove it with legacy web_editor\n        ev.__isColorpickerClick = true;\n\n        if (ev.target.dataset.colorMethod === \"hex\" && !this.selectedHexValue) {\n            ev.target.select();\n            this.selectedHexValue = ev.target.value;\n            return;\n        }\n        this.selectedHexValue = \"\";\n    }\n    onMouseUp() {\n        if (this.pickerFlag || this.sliderFlag || this.opacitySliderFlag) {\n            this._colorSelected();\n        }\n        this.pickerFlag = false;\n        this.sliderFlag = false;\n        this.opacitySliderFlag = false;\n    }\n    /**\n     * Updates color when the user starts clicking on the picker.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onMouseDownPicker(ev) {\n        this.pickerFlag = true;\n        ev.preventDefault();\n        this.onMouseMovePicker(ev);\n    }\n    /**\n     * Updates saturation and lightness values on mouse drag over picker.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onMouseMovePicker(ev) {\n        if (!this.pickerFlag) {\n            return;\n        }\n\n        const colorPickerArea = this.colorPickerAreaRef.el;\n        const rect = colorPickerArea.getClientRects()[0];\n        const top = ev.pageY - rect.top;\n        const left = ev.pageX - rect.left;\n        let saturation = Math.round((100 * left) / colorPickerArea.clientWidth);\n        let lightness = Math.round(\n            (100 * (colorPickerArea.clientHeight - top)) / colorPickerArea.clientHeight\n        );\n        saturation = clamp(saturation, 0, 100);\n        lightness = clamp(lightness, 0, 100);\n\n        this._updateHsl(this.colorComponents.hue, saturation, lightness);\n        this._updateUI();\n    }\n    /**\n     * Updates color when user starts clicking on slider.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onMouseDownSlider(ev) {\n        this.sliderFlag = true;\n        ev.preventDefault();\n        this.onMouseMoveSlider(ev);\n    }\n    /**\n     * Updates hue value on mouse drag over slider.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onMouseMoveSlider(ev) {\n        if (!this.sliderFlag) {\n            return;\n        }\n\n        const colorSlider = this.colorSliderRef.el;\n        const y = ev.pageY - colorSlider.getClientRects()[0].top;\n        let hue = Math.round((360 * y) / colorSlider.clientHeight);\n        hue = clamp(hue, 0, 360);\n\n        this._updateHsl(hue, this.colorComponents.saturation, this.colorComponents.lightness);\n        this._updateUI();\n    }\n    /**\n     * Updates opacity when user starts clicking on opacity slider.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onMouseDownOpacitySlider(ev) {\n        this.opacitySliderFlag = true;\n        ev.preventDefault();\n        this.onMouseMoveOpacitySlider(ev);\n    }\n    /**\n     * Updates opacity value on mouse drag over opacity slider.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onMouseMoveOpacitySlider(ev) {\n        if (!this.opacitySliderFlag || this.props.noTransparency) {\n            return;\n        }\n\n        const opacitySlider = this.opacitySliderRef.el;\n        const y = ev.pageY - opacitySlider.getClientRects()[0].top;\n        let opacity = Math.round(100 * (1 - y / opacitySlider.clientHeight));\n        opacity = clamp(opacity, 0, 100);\n\n        this._updateOpacity(opacity);\n        this._updateUI();\n    }\n    /**\n     * Called when input value is changed -> Updates UI: Set picker and slider\n     * position and set colors.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onChangeInputs(ev) {\n        switch (ev.target.dataset.colorMethod) {\n            case \"hex\":\n                // Handled by the \"input\" event (see \"onHexColorInput\").\n                return;\n            case \"rgb\":\n                this._updateRgba(\n                    parseInt(this.el.querySelector(\".o_red_input\").value),\n                    parseInt(this.el.querySelector(\".o_green_input\").value),\n                    parseInt(this.el.querySelector(\".o_blue_input\").value)\n                );\n                break;\n            case \"hsl\":\n                this._updateHsl(\n                    parseInt(this.el.querySelector(\".o_hue_input\").value),\n                    parseInt(this.el.querySelector(\".o_saturation_input\").value),\n                    parseInt(this.el.querySelector(\".o_lightness_input\").value)\n                );\n                break;\n            case \"opacity\":\n                this._updateOpacity(parseInt(this.el.querySelector(\".o_opacity_input\").value));\n                break;\n        }\n        this._updateUI();\n        this._colorSelected();\n    }\n    /**\n     * Called when the hex color input's input event is triggered.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    onHexColorInput(ev) {\n        const hexColorValue = ev.target.value.replaceAll(\"#\", \"\");\n        if (hexColorValue.length === 6) {\n            this._updateHex(`#${hexColorValue}`);\n            this._updateUI();\n            this._colorSelected();\n        }\n    }\n}\n", "import { clamp } from \"@web/core/utils/numbers\";\n/**\n * Lists of colors that contrast well with each other to be used in various\n * visualizations (eg. graphs/charts), both in bright and dark themes.\n */\n\nconst COLORS_ENT_BRIGHT = [\"#875A7B\", \"#A5D8D7\", \"#DCD0D9\"];\nconst COLORS_ENT_DARK = [\"#6B3E66\", \"#147875\", \"#5A395A\"];\nconst COLORS_SM = [\n    \"#4EA7F2\", // Blue\n    \"#EA6175\", // Red\n    \"#43C5B1\", // Teal\n    \"#F4A261\", // Orange\n    \"#8481DD\", // Purple\n    \"#FFD86D\", // Yellow\n];\nconst COLORS_MD = [\n    \"#4EA7F2\", // Blue #1\n    \"#3188E6\", // Blue #2\n    \"#43C5B1\", // Teal #1\n    \"#00A78D\", // Teal #2\n    \"#EA6175\", // Red #1\n    \"#CE4257\", // Red #2\n    \"#F4A261\", // Orange #1\n    \"#F48935\", // Orange #2\n    \"#8481DD\", // Purple #1\n    \"#5752D1\", // Purple #2\n    \"#FFD86D\", // Yellow #1\n    \"#FFBC2C\", // Yellow #2\n];\nconst COLORS_LG = [\n    \"#4EA7F2\", // Blue #1\n    \"#3188E6\", // Blue #2\n    \"#056BD9\", // Blue #3\n    \"#A76DBC\", // Violet #1\n    \"#7F4295\", // Violet #2\n    \"#6D2387\", // Violet #3\n    \"#EA6175\", // Red #1\n    \"#CE4257\", // Red #2\n    \"#982738\", // Red #3\n    \"#43C5B1\", // Teal #1\n    \"#00A78D\", // Teal #2\n    \"#0E8270\", // Teal #3\n    \"#F4A261\", // Orange #1\n    \"#F48935\", // Orange #2\n    \"#BE5D10\", // Orange #3\n    \"#8481DD\", // Purple #1\n    \"#5752D1\", // Purple #2\n    \"#3A3580\", // Purple #3\n    \"#A4A8B6\", // Gray #1\n    \"#7E8290\", // Gray #2\n    \"#545B70\", // Gray #3\n    \"#FFD86D\", // Yellow #1\n    \"#FFBC2C\", // Yellow #2\n    \"#C08A16\", // Yellow #3\n];\nconst COLORS_XL = [\n    \"#4EA7F2\", // Blue #1\n    \"#3188E6\", // Blue #2\n    \"#056BD9\", // Blue #3\n    \"#155193\", // Blue #4\n    \"#A76DBC\", // Violet #1\n    \"#7F4295\", // Violet #1\n    \"#6D2387\", // Violet #1\n    \"#4F1565\", // Violet #1\n    \"#EA6175\", // Red #1\n    \"#CE4257\", // Red #2\n    \"#982738\", // Red #3\n    \"#791B29\", // Red #4\n    \"#43C5B1\", // Teal #1\n    \"#00A78D\", // Teal #2\n    \"#0E8270\", // Teal #3\n    \"#105F53\", // Teal #4\n    \"#F4A261\", // Orange #1\n    \"#F48935\", // Orange #2\n    \"#BE5D10\", // Orange #3\n    \"#7D380D\", // Orange #4\n    \"#8481DD\", // Purple #1\n    \"#5752D1\", // Purple #2\n    \"#3A3580\", // Purple #3\n    \"#26235F\", // Purple #4\n    \"#A4A8B6\", // Grey #1\n    \"#7E8290\", // Grey #2\n    \"#545B70\", // Grey #3\n    \"#3F4250\", // Grey #4\n    \"#FFD86D\", // Yellow #1\n    \"#FFBC2C\", // Yellow #2\n    \"#C08A16\", // Yellow #3\n    \"#936A12\", // Yellow #4\n];\n\n/**\n * @param {string} colorScheme\n * @param {string} paletteName\n * @returns {array}\n */\nexport function getColors(colorScheme, paletteName) {\n    switch (paletteName) {\n        case \"odoo\":\n            return colorScheme === \"dark\" ? COLORS_ENT_DARK : COLORS_ENT_BRIGHT;\n        case \"sm\":\n            return COLORS_SM;\n        case \"md\":\n            return COLORS_MD;\n        case \"lg\":\n            return COLORS_LG;\n        default:\n            return COLORS_XL;\n    }\n}\n\n/**\n * @param {number} index\n * @param {string} colorScheme\n * @returns {string}\n */\nexport function getColor(index, colorScheme, paletteSizeOrName) {\n    let paletteName;\n    if (paletteSizeOrName === \"odoo\") {\n        paletteName = \"odoo\";\n    } else if (paletteSizeOrName <= 6 || paletteSizeOrName === \"sm\") {\n        paletteName = \"sm\";\n    } else if (paletteSizeOrName <= 12 || paletteSizeOrName === \"md\") {\n        paletteName = \"md\";\n    } else if (paletteSizeOrName <= 24 || paletteSizeOrName === \"lg\") {\n        paletteName = \"lg\";\n    } else {\n        paletteName = \"xl\";\n    }\n    const colors = getColors(colorScheme, paletteName);\n    return colors[index % colors.length];\n}\n\nexport const DEFAULT_BG = \"#d3d3d3\";\n\nexport function getBorderWhite(colorScheme) {\n    return colorScheme === \"dark\" ? \"rgba(38, 42, 54, .2)\" : \"rgba(249,250,251, .2)\";\n}\n\nconst RGB_REGEX = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i;\n\n/**\n * @param {string} hex\n * @param {number} opacity\n * @returns {string}\n */\nexport function hexToRGBA(hex, opacity) {\n    const rgb = RGB_REGEX.exec(hex)\n        .slice(1, 4)\n        .map((n) => parseInt(n, 16))\n        .join(\",\");\n    return `rgba(${rgb},${opacity})`;\n}\n\n/**\n * Used to return custom colors depending on the color scheme\n * @param {string} colorScheme\n * @param {string} brightModeColor\n * @param {string} darkModeColor\n * @returns {string|Number|Boolean}\n */\n\nexport function getCustomColor(colorScheme, brightModeColor, darkModeColor) {\n    if (darkModeColor === undefined) {\n        return brightModeColor;\n    } else {\n        return colorScheme === \"dark\" ? darkModeColor : brightModeColor;\n    }\n}\n\n/**\n * Used to lighten a color\n * @param {string} color\n * @param {number} factor\n * @returns {string}\n */\nexport function lightenColor(color, factor) {\n    factor = clamp(factor, 0, 1);\n\n    let r = parseInt(color.substring(1, 3), 16);\n    let g = parseInt(color.substring(3, 5), 16);\n    let b = parseInt(color.substring(5, 7), 16);\n\n    r = Math.round(r + (255 - r) * factor);\n    g = Math.round(g + (255 - g) * factor);\n    b = Math.round(b + (255 - b) * factor);\n\n    r = r.toString(16).padStart(2, \"0\");\n    g = g.toString(16).padStart(2, \"0\");\n    b = b.toString(16).padStart(2, \"0\");\n\n    return `#${r}${g}${b}`;\n}\n\n/**\n * Used to darken a color\n * @param {string} color\n * @param {number} factor\n * @returns {string}\n */\nexport function darkenColor(color, factor) {\n    factor = clamp(factor, 0, 1);\n\n    let r = parseInt(color.substring(1, 3), 16);\n    let g = parseInt(color.substring(3, 5), 16);\n    let b = parseInt(color.substring(5, 7), 16);\n\n    r = Math.round(r * (1 - factor));\n    g = Math.round(g * (1 - factor));\n    b = Math.round(b * (1 - factor));\n\n    r = r.toString(16).padStart(2, \"0\");\n    g = g.toString(16).padStart(2, \"0\");\n    b = b.toString(16).padStart(2, \"0\");\n\n    return `#${r}${g}${b}`;\n}\n", "import { Dialog } from \"../dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport const deleteConfirmationMessage = _t(\n    `Ready to make your record disappear into thin air? Are you sure?\nIt will be gone forever!\n\nThink twice before you click that 'Delete' button!`\n);\n\nexport class ConfirmationDialog extends Component {\n    static template = \"web.ConfirmationDialog\";\n    static components = { Dialog };\n    static props = {\n        close: Function,\n        title: {\n            validate: (m) => {\n                return (\n                    typeof m === \"string\" ||\n                    (typeof m === \"object\" && typeof m.toString === \"function\")\n                );\n            },\n            optional: true,\n        },\n        body: { type: String, optional: true },\n        confirm: { type: Function, optional: true },\n        confirmLabel: { type: String, optional: true },\n        confirmClass: { type: String, optional: true },\n        cancel: { type: Function, optional: true },\n        cancelLabel: { type: String, optional: true },\n        dismiss: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        confirmLabel: _t(\"Ok\"),\n        cancelLabel: _t(\"Cancel\"),\n        confirmClass: \"btn-primary\",\n        title: _t(\"Confirmation\"),\n    };\n\n    setup() {\n        this.env.dialogData.dismiss = () => this._dismiss();\n        this.modalRef = useChildRef();\n        this.isProcess = false;\n    }\n\n    async _cancel() {\n        return this.execButton(this.props.cancel);\n    }\n\n    async _confirm() {\n        return this.execButton(this.props.confirm);\n    }\n\n    async _dismiss() {\n        return this.execButton(this.props.dismiss || this.props.cancel);\n    }\n\n    setButtonsDisabled(disabled) {\n        this.isProcess = disabled;\n        if (!this.modalRef.el) {\n            return; // safety belt for stable versions\n        }\n        for (const button of [...this.modalRef.el.querySelectorAll(\".modal-footer button\")]) {\n            button.disabled = disabled;\n        }\n    }\n\n    async execButton(callback) {\n        if (this.isProcess) {\n            return;\n        }\n        this.setButtonsDisabled(true);\n        if (callback) {\n            let shouldClose;\n            try {\n                shouldClose = await callback();\n            } catch (e) {\n                this.props.close();\n                throw e;\n            }\n            if (shouldClose === false) {\n                this.setButtonsDisabled(false);\n                return;\n            }\n        }\n        this.props.close();\n    }\n}\n\nexport class AlertDialog extends ConfirmationDialog {\n    static template = \"web.AlertDialog\";\n    static props = {\n        ...ConfirmationDialog.props,\n        contentClass: { type: String, optional: true },\n    };\n    static defaultProps = {\n        ...ConfirmationDialog.defaultProps,\n        title: _t(\"Alert\"),\n    };\n}\n", "import { evaluateExpr, parseExpr } from \"./py_js/py\";\nimport { BUILTINS } from \"./py_js/py_builtin\";\nimport { evaluate } from \"./py_js/py_interpreter\";\n\n/**\n * @typedef {{[key: string]: any}} Context\n * @typedef {Context | string | undefined} ContextDescription\n */\n\n/**\n * Create an evaluated context from an arbitrary list of context representations.\n * The evaluated context in construction is used along the way to evaluate further parts.\n *\n * @param {ContextDescription[]} contexts\n * @param {Context} [initialEvaluationContext] optional evaluation context to start from.\n * @returns {Context}\n */\nexport function makeContext(contexts, initialEvaluationContext) {\n    const evaluationContext = Object.assign({}, initialEvaluationContext);\n    const context = {};\n    for (let ctx of contexts) {\n        if (ctx !== \"\") {\n            ctx = typeof ctx === \"string\" ? evaluateExpr(ctx, evaluationContext) : ctx;\n            Object.assign(context, ctx);\n            Object.assign(evaluationContext, context); // is this behavior really wanted ?\n        }\n    }\n    return context;\n}\n\n/**\n * Extract a partial list of variable names found in the AST.\n * Note that it is not complete. It is used as an heuristic to avoid\n * evaluating expressions that we know for sure will fail.\n *\n * @param {AST} ast\n * @returns string[]\n */\nfunction getPartialNames(ast) {\n    if (ast.type === 5) {\n        return [ast.value];\n    }\n    if (ast.type === 6) {\n        return getPartialNames(ast.right);\n    }\n    if (ast.type === 14 || ast.type === 7) {\n        return getPartialNames(ast.left).concat(getPartialNames(ast.right));\n    }\n    if (ast.type === 15) {\n        return getPartialNames(ast.obj);\n    }\n    return [];\n}\n\n/**\n * Allow to evaluate a context with an incomplete evaluation context. The evaluated context only\n * contains keys whose values are static or can be evaluated with the given evaluation context.\n *\n * @param {string} context\n * @param {Object} [evaluationContext={}]\n * @returns {Context}\n */\nexport function evalPartialContext(_context, evaluationContext = {}) {\n    const ast = parseExpr(_context);\n    const context = {};\n    for (const key in ast.value) {\n        const value = ast.value[key];\n        if (\n            getPartialNames(value).some((name) => !(name in evaluationContext || name in BUILTINS))\n        ) {\n            continue;\n        }\n        try {\n            context[key] = evaluate(value, evaluationContext);\n        } catch {\n            // ignore this key as we can't evaluate its value\n        }\n    }\n    return context;\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { Tooltip } from \"@web/core/tooltip/tooltip\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { Component, useRef } from \"@odoo/owl\";\n\nexport class CopyButton extends Component {\n    static template = \"web.CopyButton\";\n    static props = {\n        className: { type: String, optional: true },\n        copyText: { type: String, optional: true },\n        disabled: { type: Boolean, optional: true },\n        successText: { type: String, optional: true },\n        icon: { type: String, optional: true },\n        content: { type: [String, Object], optional: true },\n    };\n\n    setup() {\n        this.button = useRef(\"button\");\n        this.popover = usePopover(Tooltip);\n    }\n\n    showTooltip() {\n        this.popover.open(this.button.el, { tooltip: this.props.successText });\n        browser.setTimeout(this.popover.close, 800);\n    }\n\n    async onClick() {\n        let write;\n        // any kind of content can be copied into the clipboard using\n        // the appropriate native methods\n        if (typeof this.props.content === \"string\" || this.props.content instanceof String) {\n            write = (value) => browser.navigator.clipboard.writeText(value);\n        } else {\n            write = (value) => browser.navigator.clipboard.write(value);\n        }\n        try {\n            await write(this.props.content);\n        } catch (error) {\n            return browser.console.warn(error);\n        }\n        this.showTooltip();\n    }\n}\n", "import { formatFloat, humanNumber } from \"@web/core/utils/numbers\";\nimport { session } from \"@web/session\";\nimport { nbsp } from \"@web/core/utils/strings\";\n\nexport const currencies = session.currencies || {};\n// to make sure code is reading currencies from here\ndelete session.currencies;\n\nexport function getCurrency(id) {\n    return currencies[id];\n}\n\n/**\n * Returns a string representing a monetary value. The result takes into account\n * the user settings (to display the correct decimal separator, currency, ...).\n *\n * @param {number} value the value that should be formatted\n * @param {number} [currencyId] the id of the 'res.currency' to use\n * @param {Object} [options]\n *   additional options to override the values in the python description of the\n *   field.\n * @param {Object} [options.data] a mapping of field names to field values,\n *   required with options.currencyField\n * @param {boolean} [options.noSymbol] this currency has not a sympbol\n * @param {boolean} [options.humanReadable] if true, large numbers are formatted\n *   to a human readable format.\n * @param {[number, number]} [options.digits] the number of digits that should\n *   be used, instead of the default digits precision in the field.  The first\n *   number is always ignored (legacy constraint)\n * @returns {string}\n */\nexport function formatCurrency(amount, currencyId, options = {}) {\n    const currency = getCurrency(currencyId);\n    const digits = options.digits || (currency && currency.digits);\n\n    let formattedAmount;\n    if (options.humanReadable) {\n        formattedAmount = humanNumber(amount, { decimals: digits ? digits[1] : 2 });\n    } else {\n        formattedAmount = formatFloat(amount, { digits });\n    }\n\n    if (!currency || options.noSymbol) {\n        return formattedAmount;\n    }\n    const formatted = [currency.symbol, formattedAmount];\n    if (currency.position === \"after\") {\n        formatted.reverse();\n    }\n    return formatted.join(nbsp);\n}\n", "import { onPatched, onWillRender, useEffect, useRef } from \"@odoo/owl\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @param {import(\"./datetimepicker_service\").DateTimePickerHookParams} hookParams\n */\nexport function useDateTimePicker(hookParams) {\n    const datetimePicker = useService(\"datetime_picker\");\n    if (typeof hookParams.target === \"string\") {\n        const target = useRef(hookParams.target);\n        Object.defineProperty(hookParams, \"target\", {\n            get() {\n                return target.el;\n            },\n        });\n    }\n    const inputRefs = [useRef(\"start-date\"), useRef(\"end-date\")];\n    const createPopover = hookParams.createPopover ?? usePopover;\n    const getInputs = () => inputRefs.map((ref) => ref?.el);\n    const { computeBasePickerProps, state, open, focusIfNeeded, enable } = datetimePicker.create(\n        hookParams,\n        getInputs,\n        createPopover\n    );\n    onWillRender(computeBasePickerProps);\n    useEffect(enable, getInputs);\n\n    // Note: this `onPatched` callback must be called after the `useEffect` since\n    // the effect may change input values that will be selected by the patch callback.\n    onPatched(focusIfNeeded);\n    return { state, open };\n}\n", "import { Component } from \"@odoo/owl\";\nimport { omit } from \"../utils/objects\";\nimport { useDateTimePicker } from \"./datetime_hook\";\nimport { DateTimePicker } from \"./datetime_picker\";\n\n/**\n * @typedef {import(\"./datetime_picker\").DateTimePickerProps & {\n *  format?: string;\n *  id?: string;\n *  onApply?: (value: DateTime) => any;\n *  onChange?: (value: DateTime) => any;\n *  placeholder?: string;\n * }} DateTimeInputProps\n */\n\nconst dateTimeInputOwnProps = {\n    format: { type: String, optional: true },\n    id: { type: String, optional: true },\n    onChange: { type: Function, optional: true },\n    onApply: { type: Function, optional: true },\n    placeholder: { type: String, optional: true },\n};\n\n/** @extends {Component<DateTimeInputProps>} */\nexport class DateTimeInput extends Component {\n    static props = {\n        ...DateTimePicker.props,\n        ...dateTimeInputOwnProps,\n    };\n\n    static template = \"web.DateTimeInput\";\n\n    setup() {\n        const getPickerProps = () => omit(this.props, ...Object.keys(dateTimeInputOwnProps));\n\n        useDateTimePicker({\n            format: this.props.format,\n            get pickerProps() {\n                return getPickerProps();\n            },\n            onApply: (...args) => this.props.onApply?.(...args),\n            onChange: (...args) => this.props.onChange?.(...args),\n        });\n    }\n}\n", "import { Component, onWillRender, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    MAX_VALID_DATE,\n    MIN_VALID_DATE,\n    clampDate,\n    is24HourFormat,\n    isInRange,\n    isMeridiemFormat,\n    today,\n} from \"../l10n/dates\";\nimport { localization } from \"../l10n/localization\";\nimport { ensureArray } from \"../utils/arrays\";\n\nconst { DateTime, Info } = luxon;\n\n/**\n * @typedef DateItem\n * @property {string} id\n * @property {boolean} includesToday\n * @property {boolean} isOutOfRange\n * @property {boolean} isValid\n * @property {string} label\n * @property {DateRange} range\n * @property {string} extraClass\n *\n * @typedef {\"today\" | NullableDateTime} DateLimit\n *\n * @typedef {[DateTime, DateTime]} DateRange\n *\n * @typedef {luxon.DateTime} DateTime\n *\n * @typedef DateTimePickerProps\n * @property {number} [focusedDateIndex=0]\n * @property {boolean} [showWeekNumbers]\n * @property {DaysOfWeekFormat} [daysOfWeekFormat=\"short\"]\n * @property {DateLimit} [maxDate]\n * @property {PrecisionLevel} [maxPrecision=\"decades\"]\n * @property {DateLimit} [minDate]\n * @property {PrecisionLevel} [minPrecision=\"days\"]\n * @property {(value: DateTime) => any} [onSelect]\n * @property {boolean} [range]\n * @property {number} [rounding=5] the rounding in minutes, pass 0 to show seconds, pass 1 to avoid\n *  rounding minutes without displaying seconds.\n * @property {{ buttons?: any }} [slots]\n * @property {\"date\" | \"datetime\"} [type]\n * @property {NullableDateTime | NullableDateRange} [value]\n * @property {(date: DateTime) => boolean} [isDateValid]\n * @property {(date: DateTime) => string} [dayCellClass]\n *\n * @typedef {DateItem | MonthItem} Item\n *\n * @typedef MonthItem\n * @property {[string, string][]} daysOfWeek\n * @property {string} id\n * @property {number} number\n * @property {WeekItem[]} weeks\n *\n * @typedef {import(\"@web/core/l10n/dates\").NullableDateTime} NullableDateTime\n *\n * @typedef {import(\"@web/core/l10n/dates\").NullableDateRange} NullableDateRange\n *\n * @typedef PrecisionInfo\n * @property {(date: DateTime, params: Partial<DateTimePickerProps>) => string} getTitle\n * @property {(date: DateTime, params: Partial<DateTimePickerProps>) => Item[]} getItems\n * @property {string} mainTitle\n * @property {string} nextTitle\n * @property {string} prevTitle\n * @property {Record<string, number>} step\n *\n * @typedef {\"days\" | \"months\" | \"years\" | \"decades\"} PrecisionLevel\n *\n * @typedef {\"short\" | \"narrow\"} DaysOfWeekFormat\n *\n * @typedef WeekItem\n * @property {DateItem[]} days\n * @property {number} number\n */\n\n/**\n * @param {NullableDateTime} date1\n * @param {NullableDateTime} date2\n */\nconst earliest = (date1, date2) => (date1 < date2 ? date1 : date2);\n\n/**\n * @param {DateTime} date\n */\nconst getStartOfDecade = (date) => Math.floor(date.year / 10) * 10;\n\n/**\n * @param {DateTime} date\n */\nconst getStartOfCentury = (date) => Math.floor(date.year / 100) * 100;\n\n/**\n * @param {DateTime} date\n */\nconst getStartOfWeek = (date) => {\n    const { weekStart } = localization;\n    return date.set({ weekday: date.weekday < weekStart ? weekStart - 7 : weekStart });\n};\n\n/**\n * @param {NullableDateTime} date1\n * @param {NullableDateTime} date2\n */\nconst latest = (date1, date2) => (date1 > date2 ? date1 : date2);\n\n/**\n * @param {number} min\n * @param {number} max\n */\nconst numberRange = (min, max) => [...Array(max - min)].map((_, i) => i + min);\n\n/**\n * @param {NullableDateTime | \"today\"} value\n * @param {NullableDateTime | \"today\"} defaultValue\n */\nconst parseLimitDate = (value, defaultValue) =>\n    clampDate(value === \"today\" ? today() : value || defaultValue, MIN_VALID_DATE, MAX_VALID_DATE);\n\n/**\n * @param {Object} params\n * @param {boolean} [params.isOutOfRange=false]\n * @param {boolean} [params.isValid=true]\n * @param {keyof DateTime} params.label\n * @param {string} [params.extraClass]\n * @param {[DateTime, DateTime]} params.range\n * @returns {DateItem}\n */\nconst toDateItem = ({ isOutOfRange = false, isValid = true, label, range, extraClass }) => ({\n    id: range[0].toISODate(),\n    includesToday: isInRange(today(), range),\n    isOutOfRange,\n    isValid,\n    label: String(range[0][label]),\n    range,\n    extraClass,\n});\n\n/**\n * @param {DateItem[]} weekDayItems\n * @returns {WeekItem}\n */\nconst toWeekItem = (weekDayItems) => ({\n    number: weekDayItems[3].range[0].weekNumber,\n    days: weekDayItems,\n});\n\n// Time constants\nconst HOURS = numberRange(0, 24).map((hour) => [hour, String(hour)]);\nconst MINUTES = numberRange(0, 60).map((minute) => [minute, String(minute || 0).padStart(2, \"0\")]);\nconst SECONDS = [...MINUTES];\nconst MERIDIEMS = [\"AM\", \"PM\"];\n\n/**\n * Precision levels\n * @type {Map<PrecisionLevel, PrecisionInfo>}\n */\nconst PRECISION_LEVELS = new Map()\n    .set(\"days\", {\n        mainTitle: _t(\"Select month\"),\n        nextTitle: _t(\"Next month\"),\n        prevTitle: _t(\"Previous month\"),\n        step: { month: 1 },\n        getTitle: (date, { additionalMonth }) => {\n            const titles = [`${date.monthLong} ${date.year}`];\n            if (additionalMonth) {\n                const next = date.plus({ month: 1 });\n                titles.push(`${next.monthLong} ${next.year}`);\n            }\n            return titles;\n        },\n        getItems: (\n            date,\n            { additionalMonth, maxDate, minDate, showWeekNumbers, isDateValid, dayCellClass }\n        ) => {\n            const startDates = [date];\n            if (additionalMonth) {\n                startDates.push(date.plus({ month: 1 }));\n            }\n            return startDates.map((date, i) => {\n                const monthRange = [date.startOf(\"month\"), date.endOf(\"month\")];\n                /** @type {WeekItem[]} */\n                const weeks = [];\n\n                // Generate 6 weeks for current month\n                let startOfNextWeek = getStartOfWeek(monthRange[0]);\n                for (let w = 0; w < 6; w++) {\n                    const weekDayItems = [];\n                    // Generate all days of the week\n                    for (let d = 0; d < 7; d++) {\n                        const day = startOfNextWeek.plus({ day: d });\n                        const range = [day, day.endOf(\"day\")];\n                        const dayItem = toDateItem({\n                            isOutOfRange: !isInRange(day, monthRange),\n                            isValid: isInRange(range, [minDate, maxDate]) && isDateValid?.(day),\n                            label: \"day\",\n                            range,\n                            extraClass: dayCellClass?.(day) || \"\",\n                        });\n                        weekDayItems.push(dayItem);\n                        if (d === 6) {\n                            startOfNextWeek = day.plus({ day: 1 });\n                        }\n                    }\n                    weeks.push(toWeekItem(weekDayItems));\n                }\n\n                // Generate days of week labels\n                const daysOfWeek = weeks[0].days.map((d) => [\n                    d.range[0].weekdayShort,\n                    d.range[0].weekdayLong,\n                    Info.weekdays(\"narrow\", { locale: d.range[0].locale })[d.range[0].weekday - 1],\n                ]);\n                if (showWeekNumbers) {\n                    daysOfWeek.unshift([\"#\", _t(\"Week numbers\"), \"#\"]);\n                }\n\n                return {\n                    id: `__month__${i}`,\n                    number: monthRange[0].month,\n                    daysOfWeek,\n                    weeks,\n                };\n            });\n        },\n    })\n    .set(\"months\", {\n        mainTitle: _t(\"Select year\"),\n        nextTitle: _t(\"Next year\"),\n        prevTitle: _t(\"Previous year\"),\n        step: { year: 1 },\n        getTitle: (date) => String(date.year),\n        getItems: (date, { maxDate, minDate }) => {\n            const startOfYear = date.startOf(\"year\");\n            return numberRange(0, 12).map((i) => {\n                const startOfMonth = startOfYear.plus({ month: i });\n                const range = [startOfMonth, startOfMonth.endOf(\"month\")];\n                return toDateItem({\n                    isValid: isInRange(range, [minDate, maxDate]),\n                    label: \"monthShort\",\n                    range,\n                });\n            });\n        },\n    })\n    .set(\"years\", {\n        mainTitle: _t(\"Select decade\"),\n        nextTitle: _t(\"Next decade\"),\n        prevTitle: _t(\"Previous decade\"),\n        step: { year: 10 },\n        getTitle: (date) => `${getStartOfDecade(date) - 1} - ${getStartOfDecade(date) + 10}`,\n        getItems: (date, { maxDate, minDate }) => {\n            const startOfDecade = date.startOf(\"year\").set({ year: getStartOfDecade(date) });\n            return numberRange(-GRID_MARGIN, GRID_COUNT + GRID_MARGIN).map((i) => {\n                const startOfYear = startOfDecade.plus({ year: i });\n                const range = [startOfYear, startOfYear.endOf(\"year\")];\n                return toDateItem({\n                    isOutOfRange: i < 0 || i >= GRID_COUNT,\n                    isValid: isInRange(range, [minDate, maxDate]),\n                    label: \"year\",\n                    range,\n                });\n            });\n        },\n    })\n    .set(\"decades\", {\n        mainTitle: _t(\"Select century\"),\n        nextTitle: _t(\"Next century\"),\n        prevTitle: _t(\"Previous century\"),\n        step: { year: 100 },\n        getTitle: (date) => `${getStartOfCentury(date) - 10} - ${getStartOfCentury(date) + 100}`,\n        getItems: (date, { maxDate, minDate }) => {\n            const startOfCentury = date.startOf(\"year\").set({ year: getStartOfCentury(date) });\n            return numberRange(-GRID_MARGIN, GRID_COUNT + GRID_MARGIN).map((i) => {\n                const startOfDecade = startOfCentury.plus({ year: i * 10 });\n                const range = [startOfDecade, startOfDecade.plus({ year: 10, millisecond: -1 })];\n                return toDateItem({\n                    label: \"year\",\n                    isOutOfRange: i < 0 || i >= GRID_COUNT,\n                    isValid: isInRange(range, [minDate, maxDate]),\n                    range,\n                });\n            });\n        },\n    });\n\n// Other constants\nconst GRID_COUNT = 10;\nconst GRID_MARGIN = 1;\nconst NULLABLE_DATETIME_PROPERTY = [DateTime, { value: false }, { value: null }];\n\n/** @extends {Component<DateTimePickerProps>} */\nexport class DateTimePicker extends Component {\n    static props = {\n        focusedDateIndex: { type: Number, optional: true },\n        showWeekNumbers: { type: Boolean, optional: true },\n        daysOfWeekFormat: { type: String, optional: true },\n        maxDate: { type: [NULLABLE_DATETIME_PROPERTY, { value: \"today\" }], optional: true },\n        maxPrecision: {\n            type: [...PRECISION_LEVELS.keys()].map((value) => ({ value })),\n            optional: true,\n        },\n        minDate: { type: [NULLABLE_DATETIME_PROPERTY, { value: \"today\" }], optional: true },\n        minPrecision: {\n            type: [...PRECISION_LEVELS.keys()].map((value) => ({ value })),\n            optional: true,\n        },\n        onSelect: { type: Function, optional: true },\n        range: { type: Boolean, optional: true },\n        rounding: { type: Number, optional: true },\n        slots: {\n            type: Object,\n            shape: {\n                bottom_left: { type: Object, optional: true },\n                buttons: { type: Object, optional: true },\n            },\n            optional: true,\n        },\n        type: { type: [{ value: \"date\" }, { value: \"datetime\" }], optional: true },\n        value: {\n            type: [\n                NULLABLE_DATETIME_PROPERTY,\n                { type: Array, element: NULLABLE_DATETIME_PROPERTY },\n            ],\n            optional: true,\n        },\n        isDateValid: { type: Function, optional: true },\n        dayCellClass: { type: Function, optional: true },\n        tz: { type: String, optional: true },\n    };\n\n    static defaultProps = {\n        focusedDateIndex: 0,\n        daysOfWeekFormat: \"short\",\n        maxPrecision: \"decades\",\n        minPrecision: \"days\",\n        rounding: 5,\n        type: \"datetime\",\n    };\n\n    static template = \"web.DateTimePicker\";\n\n    //-------------------------------------------------------------------------\n    // Getters\n    //-------------------------------------------------------------------------\n\n    get activePrecisionLevel() {\n        return PRECISION_LEVELS.get(this.state.precision);\n    }\n\n    get isLastPrecisionLevel() {\n        return (\n            this.allowedPrecisionLevels.indexOf(this.state.precision) ===\n            this.allowedPrecisionLevels.length - 1\n        );\n    }\n\n    get titles() {\n        return ensureArray(this.title);\n    }\n\n    //-------------------------------------------------------------------------\n    // Lifecycle\n    //-------------------------------------------------------------------------\n\n    setup() {\n        this.availableHours = HOURS;\n        this.availableMinutes = MINUTES;\n        /** @type {PrecisionLevel[]} */\n        this.allowedPrecisionLevels = [];\n        /** @type {Item[]} */\n        this.items = [];\n        this.title = \"\";\n        this.shouldAdjustFocusDate = false;\n\n        this.state = useState({\n            /** @type {DateTime | null} */\n            focusDate: null,\n            /** @type {DateTime | null} */\n            hoveredDate: null,\n            /** @type {[number, number, number][]} */\n            timeValues: [],\n            /** @type {PrecisionLevel} */\n            precision: this.props.minPrecision,\n        });\n\n        this.onPropsUpdated(this.props);\n        onWillUpdateProps((nextProps) => this.onPropsUpdated(nextProps));\n\n        onWillRender(() => this.onWillRender());\n    }\n\n    /**\n     * @param {DateTimePickerProps} props\n     */\n    onPropsUpdated(props) {\n        /** @type {[NullableDateTime] | NullableDateRange} */\n        this.values = ensureArray(props.value).map((value) =>\n            value && !value.isValid ? null : value\n        );\n        this.availableHours = HOURS;\n        this.availableMinutes = MINUTES.filter((minute) => !(minute[0] % props.rounding));\n        this.availableSeconds = props.rounding ? [] : SECONDS;\n        this.allowedPrecisionLevels = this.filterPrecisionLevels(\n            props.minPrecision,\n            props.maxPrecision\n        );\n\n        this.additionalMonth = props.range && !this.env.isSmall;\n        this.maxDate = parseLimitDate(props.maxDate, MAX_VALID_DATE);\n        this.minDate = parseLimitDate(props.minDate, MIN_VALID_DATE);\n        if (this.props.type === \"date\") {\n            this.maxDate = this.maxDate.endOf(\"day\");\n            this.minDate = this.minDate.startOf(\"day\");\n        }\n\n        if (this.maxDate < this.minDate) {\n            throw new Error(`DateTimePicker error: given \"maxDate\" comes before \"minDate\".`);\n        }\n\n        const timeValues = this.values.map((val, index) => [\n            index === 1 && !this.values[1]\n                ? (val || DateTime.local()).hour + 1\n                : (val || DateTime.local()).hour,\n            val?.minute || 0,\n            val?.second || 0,\n        ]);\n        if (props.range) {\n            this.state.timeValues = timeValues;\n        } else {\n            this.state.timeValues = [];\n            this.state.timeValues[props.focusedDateIndex] = timeValues[props.focusedDateIndex];\n        }\n\n        this.shouldAdjustFocusDate = !props.range;\n        this.adjustFocus(this.values, props.focusedDateIndex);\n        this.handle12HourSystem();\n        this.state.timeValues = this.state.timeValues.map((timeValue) => timeValue.map(String));\n    }\n\n    onWillRender() {\n        const { hoveredDate } = this.state;\n        const precision = this.activePrecisionLevel;\n        const getterParams = {\n            additionalMonth: this.additionalMonth,\n            maxDate: this.maxDate,\n            minDate: this.minDate,\n            showWeekNumbers: this.props.showWeekNumbers ?? !this.props.range,\n            isDateValid: this.props.isDateValid,\n            dayCellClass: this.props.dayCellClass,\n        };\n        const referenceDate = this.state.focusDate;\n        this.title = precision.getTitle(referenceDate, getterParams);\n        this.items = precision.getItems(referenceDate, getterParams);\n\n        /** Selected Range: current values with hovered date applied */\n        this.selectedRange = [...this.values];\n        /** Highlighted Range: union of current values and selected range */\n        this.highlightedRange = [...this.values];\n\n        // Apply hovered date to selected range\n        if (hoveredDate) {\n            [this.selectedRange] = this.applyValueAtIndex(hoveredDate, this.props.focusedDateIndex);\n            if (this.props.range && this.selectedRange.every(Boolean)) {\n                this.highlightedRange = [\n                    earliest(this.selectedRange[0], this.values[0]),\n                    latest(this.selectedRange[1], this.values[1]),\n                ];\n            }\n        }\n    }\n\n    //-------------------------------------------------------------------------\n    // Methods\n    //-------------------------------------------------------------------------\n\n    /**\n     * @param {NullableDateTime[]} values\n     * @param {number} focusedDateIndex\n     */\n    adjustFocus(values, focusedDateIndex) {\n        if (!this.shouldAdjustFocusDate && this.state.focusDate) {\n            return;\n        }\n\n        let dateToFocus =\n            values[focusedDateIndex] || values[focusedDateIndex === 1 ? 0 : 1] || today();\n\n        if (\n            this.additionalMonth &&\n            focusedDateIndex === 1 &&\n            values[0] &&\n            values[1] &&\n            values[0].month !== values[1].month\n        ) {\n            dateToFocus = dateToFocus.minus({ month: 1 });\n        }\n\n        this.shouldAdjustFocusDate = false;\n        this.state.focusDate = this.clamp(dateToFocus.startOf(\"month\"));\n    }\n\n    /**\n     * @param {NullableDateTime} value\n     * @param {number} valueIndex\n     * @returns {[NullableDateRange, number]}\n     */\n    applyValueAtIndex(value, valueIndex) {\n        const result = [...this.values];\n        if (this.props.range) {\n            if (\n                (result[0] && value.endOf(\"day\") < result[0].startOf(\"day\")) ||\n                (result[1] && !result[0])\n            ) {\n                valueIndex = 0;\n            } else if (\n                (result[1] && result[1].endOf(\"day\") < value.startOf(\"day\")) ||\n                (result[0] && !result[1])\n            ) {\n                valueIndex = 1;\n            }\n        }\n        result[valueIndex] = value;\n        return [result, valueIndex];\n    }\n\n    /**\n     * @param {DateTime} value\n     */\n    clamp(value) {\n        return clampDate(value, this.minDate, this.maxDate);\n    }\n\n    /**\n     * @param {PrecisionLevel} minPrecision\n     * @param {PrecisionLevel} maxPrecision\n     */\n    filterPrecisionLevels(minPrecision, maxPrecision) {\n        const levels = [...PRECISION_LEVELS.keys()];\n        return levels.slice(levels.indexOf(minPrecision), levels.indexOf(maxPrecision) + 1);\n    }\n\n    /**\n     * Returns various flags indicating what ranges the current date item belongs\n     * to. Note that these ranges are computed differently according to the current\n     * value mode (range or single date). This is done to simplify CSS selectors.\n     * - Selected Range:\n     *      > range: current values with hovered date applied\n     *      > single date: just the hovered date\n     * - Highlighted Range:\n     *      > range: union of selection range and current values\n     *      > single date: just the current value\n     * - Current Range (range only):\n     *      > range: current start date or current end date.\n     * @param {DateItem} item\n     */\n    getActiveRangeInfo({ isOutOfRange, range }) {\n        const result = {\n            isSelected: !isOutOfRange && isInRange(this.selectedRange, range),\n            isSelectStart: false,\n            isSelectEnd: false,\n            isHighlighted: !isOutOfRange && isInRange(this.highlightedRange, range),\n            isHighlightStart: false,\n            isHighlightEnd: false,\n            isCurrent: false,\n        };\n\n        if (this.props.range) {\n            if (result.isSelected) {\n                const [selectStart, selectEnd] = this.selectedRange;\n                result.isSelectStart = !selectStart || isInRange(selectStart, range);\n                result.isSelectEnd = !selectEnd || isInRange(selectEnd, range);\n            }\n            if (result.isHighlighted) {\n                const [currentStart, currentEnd] = this.highlightedRange;\n                result.isHighlightStart = !currentStart || isInRange(currentStart, range);\n                result.isHighlightEnd = !currentEnd || isInRange(currentEnd, range);\n            }\n            result.isCurrent =\n                !isOutOfRange &&\n                (isInRange(this.values[0], range) || isInRange(this.values[1], range));\n        } else {\n            result.isSelectStart = result.isSelectEnd = result.isSelected;\n            result.isHighlightStart = result.isHighlightEnd = result.isHighlighted;\n        }\n\n        return result;\n    }\n\n    getTimeValues(valueIndex) {\n        let [hour, minute, second] = this.state.timeValues[valueIndex].map(Number);\n        if (\n            this.is12HourFormat &&\n            this.meridiems &&\n            this.state.timeValues[valueIndex][3] === \"PM\"\n        ) {\n            hour += 12;\n        }\n        return [hour, minute, second];\n    }\n\n    handle12HourSystem() {\n        if (isMeridiemFormat()) {\n            this.meridiems = MERIDIEMS.map((m) => [m, m]);\n            for (const timeValues of this.state.timeValues) {\n                if (timeValues) {\n                    timeValues.push(MERIDIEMS[Math.floor(timeValues[0] / 12) || 0]);\n                }\n            }\n        }\n        this.is12HourFormat = !is24HourFormat();\n        if (this.is12HourFormat) {\n            this.availableHours = [[0, HOURS[12][1]], ...HOURS.slice(1, 12)];\n            for (const timeValues of this.state.timeValues) {\n                if (timeValues) {\n                    timeValues[0] %= 12;\n                }\n            }\n        }\n    }\n\n    /**\n     * @param {DateItem} item\n     */\n    isSelectedDate({ range }) {\n        return this.values.some((value) => isInRange(value, range));\n    }\n\n    /**\n     * Goes to the next panel (e.g. next month if precision is \"days\").\n     * If an event is given it will be prevented.\n     * @param {PointerEvent} ev\n     */\n    next(ev) {\n        ev.preventDefault();\n        const { step } = this.activePrecisionLevel;\n        this.state.focusDate = this.clamp(this.state.focusDate.plus(step));\n    }\n\n    /**\n     * Goes to the previous panel (e.g. previous month if precision is \"days\").\n     * If an event is given it will be prevented.\n     * @param {PointerEvent} ev\n     */\n    previous(ev) {\n        ev.preventDefault();\n        const { step } = this.activePrecisionLevel;\n        this.state.focusDate = this.clamp(this.state.focusDate.minus(step));\n    }\n\n    /**\n     * Happens when an hour or a minute (or AM/PM if can apply) is selected.\n     * @param {number} valueIndex\n     */\n    selectTime(valueIndex) {\n        const value = this.values[valueIndex] || today();\n        this.validateAndSelect(value, valueIndex);\n    }\n\n    /**\n     * @param {DateTime} value\n     * @param {number} valueIndex\n     */\n    validateAndSelect(value, valueIndex) {\n        if (!this.props.onSelect) {\n            // No onSelect handler\n            return false;\n        }\n        const [result, finalIndex] = this.applyValueAtIndex(value, valueIndex);\n        if (this.props.type === \"datetime\") {\n            // Adjusts result according to the current time values\n            const [hour, minute, second] = this.getTimeValues(finalIndex);\n            result[finalIndex] = result[finalIndex].set({ hour, minute, second });\n        }\n        if (!isInRange(result[finalIndex], [this.minDate, this.maxDate])) {\n            // Date is outside range defined by min and max dates\n            return false;\n        }\n        this.props.onSelect(result.length === 2 ? result : result[0]);\n        return true;\n    }\n\n    /**\n     * Returns whether the zoom has occurred\n     * @param {DateTime} date\n     */\n    zoomIn(date) {\n        const index = this.allowedPrecisionLevels.indexOf(this.state.precision) - 1;\n        if (index in this.allowedPrecisionLevels) {\n            this.state.focusDate = this.clamp(date);\n            this.state.precision = this.allowedPrecisionLevels[index];\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Returns whether the zoom has occurred\n     */\n    zoomOut() {\n        const index = this.allowedPrecisionLevels.indexOf(this.state.precision) + 1;\n        if (index in this.allowedPrecisionLevels) {\n            this.state.precision = this.allowedPrecisionLevels[index];\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Happens when a date item is selected:\n     * - first tries to zoom in on the item\n     * - if could not zoom in: date is considered as final value and triggers a hard select\n     * @param {DateItem} dateItem\n     */\n    zoomOrSelect(dateItem) {\n        if (!dateItem.isValid) {\n            // Invalid item\n            return;\n        }\n        if (this.zoomIn(dateItem.range[0])) {\n            // Zoom was successful\n            return;\n        }\n        const [value] = dateItem.range;\n        const valueIndex = this.props.focusedDateIndex;\n        const isValid = this.validateAndSelect(value, valueIndex);\n        this.shouldAdjustFocusDate = isValid && !this.props.range;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useHotkey } from \"../hotkeys/hotkey_hook\";\nimport { DateTimePicker } from \"./datetime_picker\";\n\n/**\n * @typedef {import(\"./datetime_picker\").DateTimePickerProps} DateTimePickerProps\n *\n * @typedef DateTimePickerPopoverProps\n * @property {() => void} close\n * @property {DateTimePickerProps} pickerProps\n */\n\n/** @extends {Component<DateTimePickerPopoverProps>} */\nexport class DateTimePickerPopover extends Component {\n    static components = { DateTimePicker };\n\n    static props = {\n        close: Function, // Given by the Popover service\n        pickerProps: { type: Object, shape: DateTimePicker.props },\n    };\n\n    static template = \"web.DateTimePickerPopover\";\n\n    get isDateTimeRange() {\n        return (\n            this.props.pickerProps.type === \"datetime\" ||\n            Array.isArray(this.props.pickerProps.value)\n        );\n    }\n\n    //-------------------------------------------------------------------------\n    // Lifecycle\n    //-------------------------------------------------------------------------\n\n    setup() {\n        useHotkey(\"enter\", () => this.props.close());\n    }\n}\n", "import { markRaw, reactive } from \"@odoo/owl\";\nimport { areDatesEqual, formatDate, formatDateTime, parseDate, parseDateTime } from \"../l10n/dates\";\nimport { makePopover } from \"../popover/popover_hook\";\nimport { registry } from \"../registry\";\nimport { ensureArray, zip, zipWith } from \"../utils/arrays\";\nimport { deepCopy, shallowEqual } from \"../utils/objects\";\nimport { DateTimePicker } from \"./datetime_picker\";\nimport { DateTimePickerPopover } from \"./datetime_picker_popover\";\n\n/**\n * @typedef {luxon.DateTime} DateTime\n *\n * @typedef DateTimePickerHookParams\n * @property {string} [format]\n * @property {(value: DateTimePickerProps[\"value\"]) => any} [onChange] callback\n *  invoked every time the hook updates the reactive value, either through the inputs\n *  or the picker.\n * @property {(value: DateTimePickerProps[\"value\"]) => any} [onApply] callback\n *  invoked once the value is committed: this is either when all inputs received\n *  a \"change\" event or when the datetime picker popover has been closed.\n * @property {DateTimePickerProps} pickerProps\n * @property {string | ReturnType<typeof import(\"@odoo/owl\").useRef>} [target]\n * @property {(component, options) => import(\"../popover/popover_hook\").PopoverHookReturnType} [createPopover]\n * @property {() => boolean} [ensureVisibility=() => env.isSmall]\n * @property {boolean} [showSeconds]\n *\n * @typedef {import(\"./datetime_picker\").DateTimePickerProps} DateTimePickerProps\n */\n\n/**\n * @template {HTMLElement} T\n * @typedef {{ el: T | null }} OwlRef\n */\n\n/** @type {typeof shallowEqual} */\nconst arePropsEqual = (obj1, obj2) =>\n    shallowEqual(obj1, obj2, (a, b) => areDatesEqual(a, b) || shallowEqual(a, b));\n\nconst FOCUS_CLASSNAME = \"text-primary\";\n\nconst formatters = {\n    date: formatDate,\n    datetime: formatDateTime,\n};\n\nconst listenedElements = new WeakSet();\n\nconst parsers = {\n    date: parseDate,\n    datetime: parseDateTime,\n};\n\nexport const datetimePickerService = {\n    dependencies: [\"popover\"],\n    start(env, { popover: popoverService }) {\n        return {\n            /**\n             * @param {DateTimePickerHookParams} hookParams\n             */\n            create: (hookParams, getInputs = () => [hookParams.target, null]) => {\n                const createPopover =\n                    hookParams.createPopover ??\n                    ((...args) => makePopover(popoverService.add, ...args));\n                const ensureVisibility = hookParams.ensureVisibility ?? (() => env.isSmall);\n                const popover = createPopover(DateTimePickerPopover, {\n                    onClose: () => {\n                        if (!allowOnClose) {\n                            return;\n                        }\n                        updateValueFromInputs();\n                        apply();\n                        setFocusClass(null);\n                        if (restoreTargetMargin) {\n                            restoreTargetMargin();\n                            restoreTargetMargin = null;\n                        }\n                    },\n                });\n                // Hook methods\n\n                /**\n                 * Wrapper method on the \"onApply\" callback to only call it when the\n                 * value has changed, and set other internal variables accordingly.\n                 */\n                const apply = () => {\n                    if (areDatesEqual(lastInitialProps?.value, deepCopy(pickerProps.value))) {\n                        return;\n                    }\n\n                    inputsChanged = ensureArray(pickerProps.value).map(() => false);\n\n                    hookParams.onApply?.(pickerProps.value);\n                };\n\n                const computeBasePickerProps = () => {\n                    const nextInitialProps = markValuesRaw(hookParams.pickerProps);\n                    const propsCopy = deepCopy(nextInitialProps);\n\n                    if (lastInitialProps && arePropsEqual(lastInitialProps, propsCopy)) {\n                        return;\n                    }\n\n                    lastInitialProps = propsCopy;\n                    inputsChanged = ensureArray(lastInitialProps.value).map(() => false);\n\n                    for (const [key, value] of Object.entries(nextInitialProps)) {\n                        if (pickerProps[key] !== value && !areDatesEqual(pickerProps[key], value)) {\n                            pickerProps[key] = value;\n                        }\n                    }\n                };\n\n                /**\n                 * Ensures the current focused input (indicated by `pickerProps.focusedDateIndex`)\n                 * is actually focused.\n                 */\n                const focusActiveInput = () => {\n                    const inputEl = getInput(pickerProps.focusedDateIndex);\n                    if (!inputEl) {\n                        shouldFocus = true;\n                        return;\n                    }\n\n                    const { activeElement } = inputEl.ownerDocument;\n                    if (activeElement !== inputEl) {\n                        inputEl.focus();\n                    }\n\n                    setInputFocus(inputEl);\n                };\n\n                /**\n                 * @param {number} valueIndex\n                 * @returns {HTMLInputElement | null}\n                 */\n                const getInput = (valueIndex) => {\n                    const el = getInputs()[valueIndex];\n                    if (el && document.body.contains(el)) {\n                        return el;\n                    }\n                    return null;\n                };\n\n                /**\n                 * Returns the appropriate root element to attach the popover:\n                 * - if the value is a range: the closest common parent of the two inputs\n                 * - if not: the first input\n                 */\n                const getPopoverTarget = () => {\n                    if (hookParams.target) {\n                        return hookParams.target;\n                    }\n                    if (pickerProps.range) {\n                        let parentElement = getInput(0).parentElement;\n                        const inputEls = getInputs();\n                        while (\n                            parentElement &&\n                            !inputEls.every((inputEl) => parentElement.contains(inputEl))\n                        ) {\n                            parentElement = parentElement.parentElement;\n                        }\n                        return parentElement || getInput(0);\n                    } else {\n                        return getInput(0);\n                    }\n                };\n\n                /**\n                 * @template {object} T\n                 * @param {T} obj\n                 */\n                const markValuesRaw = (obj) => {\n                    /** @type {T} */\n                    const copy = {};\n                    for (const [key, value] of Object.entries(obj)) {\n                        if (value && typeof value === \"object\") {\n                            copy[key] = markRaw(value);\n                        } else {\n                            copy[key] = value;\n                        }\n                    }\n                    return copy;\n                };\n\n                /**\n                 * Inputs \"change\" event handler. This will trigger an \"onApply\" callback if\n                 * one of the following is true:\n                 * - there is only one input;\n                 * - the popover is closed;\n                 * - the other input has also changed.\n                 *\n                 * @param {Event} ev\n                 */\n                const onInputChange = (ev) => {\n                    updateValueFromInputs();\n                    inputsChanged[ev.target === getInput(1) ? 1 : 0] = true;\n                    if (!popover.isOpen || inputsChanged.every(Boolean)) {\n                        saveAndClose();\n                    }\n                };\n\n                /**\n                 * @param {PointerEvent} ev\n                 */\n                const onInputClick = ({ target }) => {\n                    openPicker(target === getInput(1) ? 1 : 0);\n                };\n\n                /**\n                 * @param {FocusEvent} ev\n                 */\n                const onInputFocus = ({ target }) => {\n                    pickerProps.focusedDateIndex = target === getInput(1) ? 1 : 0;\n                    setInputFocus(target);\n                };\n\n                /**\n                 * @param {KeyboardEvent} ev\n                 */\n                const onInputKeydown = (ev) => {\n                    if (ev.key == \"Enter\" && ev.ctrlKey) {\n                        ev.preventDefault();\n                        updateValueFromInputs();\n                        return openPicker(ev.target === getInput(1) ? 1 : 0);\n                    }\n                    switch (ev.key) {\n                        case \"Enter\":\n                        case \"Escape\": {\n                            return saveAndClose();\n                        }\n                        case \"Tab\": {\n                            if (\n                                !getInput(0) ||\n                                !getInput(1) ||\n                                ev.target !== getInput(ev.shiftKey ? 1 : 0)\n                            ) {\n                                return saveAndClose();\n                            }\n                        }\n                    }\n                };\n\n                /**\n                 * @param {number} inputIndex Input from which to open the picker\n                 */\n                const openPicker = (inputIndex) => {\n                    pickerProps.focusedDateIndex = inputIndex;\n\n                    if (!popover.isOpen) {\n                        const popoverTarget = getPopoverTarget();\n                        if (ensureVisibility()) {\n                            const { marginBottom } = popoverTarget.style;\n                            // Adds enough space for the popover to be displayed below the target\n                            // even on small screens.\n                            popoverTarget.style.marginBottom = `100vh`;\n                            popoverTarget.scrollIntoView(true);\n                            restoreTargetMargin = async () => {\n                                popoverTarget.style.marginBottom = marginBottom;\n                            };\n                        }\n                        popover.open(popoverTarget, { pickerProps });\n                    }\n\n                    focusActiveInput();\n                };\n\n                /**\n                 * @template {\"format\" | \"parse\"} T\n                 * @param {T} operation\n                 * @param {T extends \"format\" ? DateTime : string} value\n                 * @returns {[T extends \"format\" ? string : DateTime, null] | [null, Error]}\n                 */\n                const safeConvert = (operation, value) => {\n                    const { type } = pickerProps;\n                    const convertFn = (operation === \"format\" ? formatters : parsers)[type];\n                    const options = { tz: pickerProps.tz, format: hookParams.format };\n                    if (operation === \"format\") {\n                        options.showSeconds = hookParams.showSeconds ?? true;\n                        options.condensed = hookParams.condensed || false;\n                    }\n                    try {\n                        return [convertFn(value, options), null];\n                    } catch (error) {\n                        if (error?.name === \"ConversionError\") {\n                            return [null, error];\n                        } else {\n                            throw error;\n                        }\n                    }\n                };\n\n                /**\n                 * Wrapper method to ensure the \"onApply\" callback is called, either:\n                 * - by closing the popover (if any);\n                 * - or by directly calling \"apply\", without updating the values.\n                 */\n                const saveAndClose = () => {\n                    if (popover.isOpen) {\n                        // apply will be done in the \"onClose\" callback\n                        popover.close();\n                    } else {\n                        apply();\n                    }\n                };\n\n                /**\n                 * Updates class names on given inputs according to the currently selected input.\n                 *\n                 * @param {HTMLInputElement | null} input\n                 */\n                const setFocusClass = (input) => {\n                    for (const el of getInputs()) {\n                        if (el) {\n                            el.classList.toggle(FOCUS_CLASSNAME, popover.isOpen && el === input);\n                        }\n                    }\n                };\n\n                /**\n                 * Applies class names to all inputs according to whether they are focused or not.\n                 *\n                 * @param {HTMLInputElement} inputEl\n                 */\n                const setInputFocus = (inputEl) => {\n                    inputEl.selectionStart = 0;\n                    inputEl.selectionEnd = inputEl.value.length;\n\n                    setFocusClass(inputEl);\n\n                    shouldFocus = false;\n                };\n\n                /**\n                 * Synchronizes the given input with the given value.\n                 *\n                 * @param {HTMLInputElement} el\n                 * @param {DateTime} value\n                 */\n                const updateInput = (el, value) => {\n                    if (!el) {\n                        return;\n                    }\n                    const [formattedValue] = safeConvert(\"format\", value);\n                    el.value = formattedValue || \"\";\n                };\n\n                /**\n                 * @param {DateTimePickerProps[\"value\"]} value\n                 */\n                const updateValue = (value) => {\n                    const previousValue = pickerProps.value;\n                    pickerProps.value = value;\n\n                    if (areDatesEqual(previousValue, pickerProps.value)) {\n                        return;\n                    }\n\n                    if (pickerProps.range) {\n                        // When in range: compare each individual value\n                        const [prevStart, prevEnd] = ensureArray(previousValue);\n                        const [nextStart, nextEnd] = ensureArray(pickerProps.value);\n                        if (\n                            (pickerProps.focusedDateIndex === 0 &&\n                                areDatesEqual(prevEnd, nextEnd)) ||\n                            (pickerProps.focusedDateIndex === 1 &&\n                                areDatesEqual(prevStart, nextStart))\n                        ) {\n                            pickerProps.focusedDateIndex =\n                                pickerProps.focusedDateIndex === 1 ? 0 : 1;\n                        }\n                    }\n\n                    hookParams.onChange?.(pickerProps.value);\n                };\n\n                const updateValueFromInputs = () => {\n                    const values = zipWith(\n                        getInputs(),\n                        ensureArray(pickerProps.value),\n                        (el, currentValue) => {\n                            if (!el) {\n                                return currentValue;\n                            }\n                            const [parsedValue, error] = safeConvert(\"parse\", el.value);\n                            if (error) {\n                                updateInput(el, currentValue);\n                                return currentValue;\n                            } else {\n                                return parsedValue;\n                            }\n                        }\n                    );\n                    updateValue(values.length === 2 ? values : values[0]);\n                };\n\n                // Hook variables\n\n                /** @type {DateTimePickerProps} */\n                const rawPickerProps = {\n                    ...DateTimePicker.defaultProps,\n                    onSelect: (value) => {\n                        value &&= markRaw(value);\n                        updateValue(value);\n                        if (!pickerProps.range && pickerProps.type === \"date\") {\n                            saveAndClose();\n                        }\n                    },\n                    ...markValuesRaw(hookParams.pickerProps),\n                };\n                const pickerProps = reactive(rawPickerProps, () => {\n                    // Resets the popover position when switching from single date to a range\n                    // or vice-versa\n                    const currentIsRange = pickerProps.range;\n                    if (popover.isOpen && lastIsRange !== currentIsRange) {\n                        allowOnClose = false;\n                        popover.open(getPopoverTarget(), { pickerProps });\n                        allowOnClose = true;\n                    }\n                    lastIsRange = currentIsRange;\n\n                    // Update inputs\n                    for (const [el, value] of zip(\n                        getInputs(),\n                        ensureArray(pickerProps.value),\n                        true\n                    )) {\n                        if (el) {\n                            updateInput(el, value);\n                        }\n                    }\n\n                    shouldFocus = true;\n                });\n\n                /** Decides whether the popover 'onClose' callback can be called */\n                let allowOnClose = true;\n                /** @type {boolean[]} */\n                let inputsChanged = [];\n                /** @type {DateTimePickerProps | null} */\n                let lastInitialProps = null;\n                let lastIsRange = pickerProps.range;\n                /** @type {(() => void) | null} */\n                let restoreTargetMargin = null;\n                let shouldFocus = false;\n\n                return {\n                    state: pickerProps,\n                    open: openPicker,\n                    computeBasePickerProps,\n                    focusIfNeeded() {\n                        if (popover.isOpen && shouldFocus) {\n                            focusActiveInput();\n                        }\n                    },\n                    enable() {\n                        let editableInputs = 0;\n                        for (const [el, value] of zip(\n                            getInputs(),\n                            ensureArray(pickerProps.value),\n                            true\n                        )) {\n                            updateInput(el, value);\n                            if (el && !el.disabled && !el.readOnly && !listenedElements.has(el)) {\n                                listenedElements.add(el);\n                                el.addEventListener(\"change\", onInputChange);\n                                el.addEventListener(\"click\", onInputClick);\n                                el.addEventListener(\"focus\", onInputFocus);\n                                el.addEventListener(\"keydown\", onInputKeydown);\n                                editableInputs++;\n                            }\n                        }\n                        const calendarIconGroupEl = getInput(0)?.parentElement.querySelector(\n                            \".o_input_group_date_icon\"\n                        );\n                        if (calendarIconGroupEl) {\n                            calendarIconGroupEl.classList.add(\"cursor-pointer\");\n                            calendarIconGroupEl.addEventListener(\"click\", () => openPicker(0));\n                        }\n                        if (!editableInputs && popover.isOpen) {\n                            saveAndClose();\n                        }\n                        return () => {};\n                    },\n                    get isOpen() {\n                        return popover.isOpen;\n                    },\n                };\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"datetime_picker\", datetimePickerService);\n", "import { user } from \"@web/core/user\";\nimport { registry } from \"../registry\";\n\nimport { useEffect, useEnv, useSubEnv } from \"@odoo/owl\";\nconst debugRegistry = registry.category(\"debug\");\n\nconst getAccessRights = async () => {\n    const rightsToCheck = {\n        \"ir.ui.view\": \"write\",\n        \"ir.rule\": \"read\",\n        \"ir.model.access\": \"read\",\n    };\n    const proms = Object.entries(rightsToCheck).map(([model, operation]) => {\n        return user.checkAccessRight(model, operation);\n    });\n    const [canEditView, canSeeRecordRules, canSeeModelAccess] = await Promise.all(proms);\n    const accessRights = { canEditView, canSeeRecordRules, canSeeModelAccess };\n    return accessRights;\n};\n\nclass DebugContext {\n    constructor(defaultCategories) {\n        this.categories = new Map(defaultCategories.map((cat) => [cat, [{}]]));\n    }\n\n    activateCategory(category, context) {\n        const contexts = this.categories.get(category) || new Set();\n        contexts.add(context);\n        this.categories.set(category, contexts);\n\n        return () => {\n            contexts.delete(context);\n            if (contexts.size === 0) {\n                this.categories.delete(category);\n            }\n        };\n    }\n\n    async getItems(env) {\n        const accessRights = await getAccessRights();\n        return [...this.categories.entries()]\n            .flatMap(([category, contexts]) => {\n                return debugRegistry\n                    .category(category)\n                    .getAll()\n                    .map((factory) => factory(Object.assign({ env, accessRights }, ...contexts)));\n            })\n            .filter(Boolean)\n            .sort((x, y) => {\n                const xSeq = x.sequence || 1000;\n                const ySeq = y.sequence || 1000;\n                return xSeq - ySeq;\n            });\n    }\n}\n\nconst debugContextSymbol = Symbol(\"debugContext\");\nexport function createDebugContext({ categories = [] } = {}) {\n    return { [debugContextSymbol]: new DebugContext(categories) };\n}\n\nexport function useOwnDebugContext({ categories = [] } = {}) {\n    useSubEnv(createDebugContext({ categories }));\n}\n\nexport function useEnvDebugContext() {\n    const debugContext = useEnv()[debugContextSymbol];\n    if (!debugContext) {\n        throw new Error(\"There is no debug context available in the current environment.\");\n    }\n    return debugContext;\n}\n\nexport function useDebugCategory(category, context = {}) {\n    const env = useEnv();\n    if (env.debug) {\n        const debugContext = useEnvDebugContext();\n        useEffect(\n            () => debugContext.activateCategory(category, context),\n            () => []\n        );\n    }\n}\n", "import { useEnvDebugContext } from \"./debug_context\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { groupBy, sortBy } from \"@web/core/utils/arrays\";\n\nimport { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nconst debugSectionRegistry = registry.category(\"debug_section\");\n\ndebugSectionRegistry\n    .add(\"record\", { label: _t(\"Record\"), sequence: 10 })\n    .add(\"records\", { label: _t(\"Records\"), sequence: 10 })\n    .add(\"ui\", { label: _t(\"User Interface\"), sequence: 20 })\n    .add(\"security\", { label: _t(\"Security\"), sequence: 30 })\n    .add(\"testing\", { label: _t(\"Testing\"), sequence: 40 })\n    .add(\"tools\", { label: _t(\"Tools\"), sequence: 50 });\n\nexport class DebugMenuBasic extends Component {\n    static template = \"web.DebugMenu\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {};\n\n    setup() {\n        this.debugContext = useEnvDebugContext();\n    }\n\n    async loadGroupedItems() {\n        const items = await this.debugContext.getItems(this.env);\n        const sections = groupBy(items, (item) => item.section || \"\");\n        this.sectionEntries = sortBy(\n            Object.entries(sections),\n            ([section]) => debugSectionRegistry.get(section, { sequence: 50 }).sequence\n        );\n    }\n\n    getSectionLabel(section) {\n        return debugSectionRegistry.get(section, { label: section }).label;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { router } from \"@web/core/browser/router\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\n\nfunction activateTestsAssetsDebugging({ env }) {\n    if (String(router.current.debug).includes(\"tests\")) {\n        return;\n    }\n\n    return {\n        type: \"item\",\n        description: _t(\"Activate Test Mode\"),\n        callback: () => {\n            router.pushState({ debug: \"assets,tests\" }, { reload: true });\n        },\n        sequence: 580,\n        section: \"tools\",\n    };\n}\n\nexport function regenerateAssets({ env }) {\n    return {\n        type: \"item\",\n        description: _t(\"Regenerate Assets\"),\n        callback: async () => {\n            await env.services.orm.call(\"ir.attachment\", \"regenerate_assets_bundles\");\n            browser.location.reload();\n        },\n        sequence: 550,\n        section: \"tools\",\n    };\n}\n\nfunction becomeSuperuser({ env }) {\n    const becomeSuperuserURL = browser.location.origin + \"/web/become\";\n    return {\n        type: \"item\",\n        description: _t(\"Become Superuser\"),\n        hide: !user.isAdmin,\n        href: becomeSuperuserURL,\n        callback: () => {\n            browser.open(becomeSuperuserURL, \"_self\");\n        },\n        sequence: 560,\n        section: \"tools\",\n    };\n}\n\nfunction leaveDebugMode() {\n    return {\n        type: \"item\",\n        description: _t(\"Leave Debug Mode\"),\n        callback: () => {\n            router.pushState({ debug: 0 }, { reload: true });\n        },\n        sequence: 650,\n    };\n}\n\nregistry\n    .category(\"debug\")\n    .category(\"default\")\n    .add(\"regenerateAssets\", regenerateAssets)\n    .add(\"becomeSuperuser\", becomeSuperuser)\n    .add(\"activateTestsAssetsDebugging\", activateTestsAssetsDebugging)\n    .add(\"leaveDebugMode\", leaveDebugMode);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"../registry\";\nimport { browser } from \"../browser/browser\";\nimport { router } from \"../browser/router\";\n\nconst commandProviderRegistry = registry.category(\"command_provider\");\n\ncommandProviderRegistry.add(\"debug\", {\n    provide: (env, options) => {\n        const result = [];\n        if (env.debug) {\n            if (!env.debug.includes(\"assets\")) {\n                result.push({\n                    action() {\n                        router.pushState({ debug: \"assets\" }, { reload: true });\n                    },\n                    category: \"debug\",\n                    name: _t(\"Activate debug mode (with assets)\"),\n                });\n            }\n            result.push({\n                action() {\n                    router.pushState({ debug: 0 }, { reload: true });\n                },\n                category: \"debug\",\n                name: _t(\"Deactivate debug mode\"),\n            });\n            result.push({\n                action() {\n                    browser.open(\"/web/tests?debug=assets\");\n                },\n                category: \"debug\",\n                name: _t(\"Run Unit Tests\"),\n            });\n        } else {\n            const debugKey = \"debug\";\n            if (options.searchValue.toLowerCase() === debugKey) {\n                result.push({\n                    action() {\n                        router.pushState({ debug: \"1\" }, { reload: true });\n                    },\n                    category: \"debug\",\n                    name: `${_t(\"Activate debug mode\")} (${debugKey})`,\n                });\n                result.push({\n                    action() {\n                        router.pushState({ debug: \"assets\" }, { reload: true });\n                    },\n                    category: \"debug\",\n                    name: `${_t(\"Activate debug mode (with assets)\")} (${debugKey})`,\n                });\n            }\n        }\n        return result;\n    },\n});\n", "export function editModelDebug(env, title, model, id) {\n    return env.services.action.doAction({\n        res_model: model,\n        res_id: id,\n        name: title,\n        type: \"ir.actions.act_window\",\n        views: [[false, \"form\"]],\n        view_mode: \"form\",\n        target: \"current\",\n    });\n}\n", "import { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { useActiveElement } from \"../ui/ui_service\";\nimport { useForwardRefToParent } from \"@web/core/utils/hooks\";\nimport { Component, onWillDestroy, useChildSubEnv, useExternalListener, useState } from \"@odoo/owl\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { makeDraggableHook } from \"../utils/draggable_hook_builder_owl\";\n\nconst useDialogDraggable = makeDraggableHook({\n    name: \"useDialogDraggable\",\n    onWillStartDrag({ ctx, addCleanup, addStyle, getRect }) {\n        const { height, width } = getRect(ctx.current.element);\n        ctx.current.container = document.createElement(\"div\");\n        addStyle(ctx.current.container, {\n            position: \"fixed\",\n            top: \"0\",\n            bottom: `${70 - height}px`,\n            left: `${70 - width}px`,\n            right: `${70 - width}px`,\n        });\n        ctx.current.element.after(ctx.current.container);\n        addCleanup(() => ctx.current.container.remove());\n    },\n    onDrop({ ctx, getRect }) {\n        const { top, left } = getRect(ctx.current.element);\n        return {\n            left: left - ctx.current.elementRect.left,\n            top: top - ctx.current.elementRect.top,\n        };\n    },\n});\n\nexport class Dialog extends Component {\n    static template = \"web.Dialog\";\n    static props = {\n        contentClass: { type: String, optional: true },\n        bodyClass: { type: String, optional: true },\n        fullscreen: { type: Boolean, optional: true },\n        footer: { type: Boolean, optional: true },\n        header: { type: Boolean, optional: true },\n        size: {\n            type: String,\n            optional: true,\n            validate: (s) => [\"sm\", \"md\", \"lg\", \"xl\", \"fs\", \"fullscreen\"].includes(s),\n        },\n        technical: { type: Boolean, optional: true },\n        title: { type: String, optional: true },\n        modalRef: { type: Function, optional: true },\n        slots: {\n            type: Object,\n            shape: {\n                default: Object, // Content is not optional\n                header: { type: Object, optional: true },\n                footer: { type: Object, optional: true },\n            },\n        },\n        withBodyPadding: { type: Boolean, optional: true },\n        onExpand: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        contentClass: \"\",\n        bodyClass: \"\",\n        fullscreen: false,\n        footer: true,\n        header: true,\n        size: \"lg\",\n        technical: true,\n        title: \"Odoo\",\n        withBodyPadding: true,\n    };\n\n    setup() {\n        this.modalRef = useForwardRefToParent(\"modalRef\");\n        useActiveElement(\"modalRef\");\n        this.data = useState(this.env.dialogData);\n        useHotkey(\"escape\", () => this.onEscape());\n        useHotkey(\n            \"control+enter\",\n            () => {\n                const btns = document.querySelectorAll(\n                    \".o_dialog:not(.o_inactive_modal) .modal-footer button\"\n                );\n                const firstVisibleBtn = Array.from(btns).find((btn) => {\n                    const styles = getComputedStyle(btn);\n                    return styles.display !== \"none\";\n                });\n                if (firstVisibleBtn) {\n                    firstVisibleBtn.click();\n                }\n            },\n            { bypassEditableProtection: true }\n        );\n        this.id = `dialog_${this.data.id}`;\n        useChildSubEnv({ inDialog: true, dialogId: this.id });\n        this.isMovable = this.props.header;\n        if (this.isMovable) {\n            this.position = useState({ left: 0, top: 0 });\n            useDialogDraggable({\n                enable: () => !this.env.isSmall,\n                ref: this.modalRef,\n                elements: \".modal-content\",\n                handle: \".modal-header\",\n                ignore: \"button, input\",\n                edgeScrolling: { enabled: false },\n                onDrop: ({ top, left }) => {\n                    this.position.left += left;\n                    this.position.top += top;\n                },\n            });\n            const throttledResize = throttleForAnimation(this.onResize.bind(this));\n            useExternalListener(window, \"resize\", throttledResize);\n        }\n        onWillDestroy(() => {\n            if (this.env.isSmall) {\n                this.data.scrollToOrigin();\n            }\n        });\n    }\n\n    get isFullscreen() {\n        return this.props.fullscreen || this.env.isSmall;\n    }\n\n    get contentStyle() {\n        if (this.isMovable) {\n            return `top: ${this.position.top}px; left: ${this.position.left}px;`;\n        }\n        return \"\";\n    }\n\n    onResize() {\n        this.position.left = 0;\n        this.position.top = 0;\n    }\n\n    onEscape() {\n        return this.dismiss();\n    }\n\n    async dismiss() {\n        if (this.data.dismiss) {\n            await this.data.dismiss();\n        }\n        return this.data.close();\n    }\n}\n", "import { Component, markRaw, reactive, useChildSubEnv, xml } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nclass DialogWrapper extends Component {\n    static template = xml`<t t-component=\"props.subComponent\" t-props=\"props.subProps\" />`;\n    static props = [\"*\"];\n    setup() {\n        useChildSubEnv({ dialogData: this.props.subEnv });\n    }\n}\n\n/**\n *  @typedef {{\n *      onClose?(): void;\n *  }} DialogServiceInterfaceAddOptions\n */\n/**\n *  @typedef {{\n *      add(\n *          Component: typeof import(\"@odoo/owl\").Component,\n *          props: {},\n *          options?: DialogServiceInterfaceAddOptions\n *      ): () => void;\n *  }} DialogServiceInterface\n */\n\nexport const dialogService = {\n    dependencies: [\"overlay\"],\n    /** @returns {DialogServiceInterface} */\n    start(env, { overlay }) {\n        const stack = [];\n        let nextId = 0;\n\n        const deactivate = () => {\n            for (const subEnv of stack) {\n                subEnv.isActive = false;\n            }\n        };\n\n        const add = (dialogClass, props, options = {}) => {\n            const id = nextId++;\n            const close = () => remove();\n            const subEnv = reactive({\n                id,\n                close,\n                isActive: true,\n            });\n\n            deactivate();\n            stack.push(subEnv);\n            document.body.classList.add(\"modal-open\");\n\n            const scrollOrigin = { top: window.scrollY, left: window.scrollX };\n            subEnv.scrollToOrigin = () => {\n                if (!stack.length) {\n                    window.scrollTo(scrollOrigin);\n                }\n            };\n\n            const remove = overlay.add(\n                DialogWrapper,\n                {\n                    subComponent: dialogClass,\n                    subProps: markRaw({ ...props, close }),\n                    subEnv,\n                },\n                {\n                    onRemove: () => {\n                        stack.pop();\n                        deactivate();\n                        if (stack.length) {\n                            stack.at(-1).isActive = true;\n                        } else {\n                            document.body.classList.remove(\"modal-open\");\n                        }\n                        options.onClose?.();\n                    },\n                    rootId: options.context?.root?.el.getRootNode()?.host?.id,\n                }\n            );\n\n            return remove;\n        };\n\n        function closeAll() {\n            for (const dialog of [...stack].reverse()) {\n                dialog.close();\n            }\n        }\n\n        return { add, closeAll };\n    },\n};\n\nregistry.category(\"services\").add(\"dialog\", dialogService);\n", "import { shallowEqual } from \"@web/core/utils/arrays\";\nimport { evaluate, formatAST, parseExpr } from \"./py_js/py\";\nimport { toPyValue } from \"./py_js/py_utils\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\n\n/**\n * @typedef {import(\"./py_js/py_parser\").AST} AST\n * @typedef {[string | 0 | 1, string, any]} Condition\n * @typedef {(\"&\" | \"|\" | \"!\" | Condition)[]} DomainListRepr\n * @typedef {DomainListRepr | string | Domain} DomainRepr\n */\n\nexport class InvalidDomainError extends Error {}\n\n/**\n * Javascript representation of an Odoo domain\n */\nexport class Domain {\n    /**\n     * Combine various domains together with a given operator\n     * @param {DomainRepr[]} domains\n     * @param {\"AND\" | \"OR\"} operator\n     * @returns {Domain}\n     */\n    static combine(domains, operator) {\n        if (domains.length === 0) {\n            return new Domain([]);\n        }\n        const domain1 = domains[0] instanceof Domain ? domains[0] : new Domain(domains[0]);\n        if (domains.length === 1) {\n            return domain1;\n        }\n        const domain2 = Domain.combine(domains.slice(1), operator);\n        const result = new Domain([]);\n        const astValues1 = domain1.ast.value;\n        const astValues2 = domain2.ast.value;\n        const op = operator === \"AND\" ? \"&\" : \"|\";\n        const combinedAST = { type: 4 /* List */, value: astValues1.concat(astValues2) };\n        result.ast = normalizeDomainAST(combinedAST, op);\n        return result;\n    }\n\n    /**\n     * Combine various domains together with `AND` operator\n     * @param {DomainRepr} domains\n     * @returns {Domain}\n     */\n    static and(domains) {\n        return Domain.combine(domains, \"AND\");\n    }\n\n    /**\n     * Combine various domains together with `OR` operator\n     * @param {DomainRepr} domains\n     * @returns {Domain}\n     */\n    static or(domains) {\n        return Domain.combine(domains, \"OR\");\n    }\n\n    /**\n     * Return the negation of the domain\n     * @returns {Domain}\n     */\n    static not(domain) {\n        const result = new Domain(domain);\n        result.ast.value.unshift({ type: 1, value: \"!\" });\n        return result;\n    }\n\n    /**\n     * Return a new domain with `neutralized` leaves (for the leaves that are applied on the field that are part of\n     * keysToRemove).\n     * @param {DomainRepr} domain\n     * @param {string[]} keysToRemove\n     * @return {Domain}\n     */\n    static removeDomainLeaves(domain, keysToRemove) {\n        function processLeaf(elements, idx, operatorCtx, newDomain) {\n            const leaf = elements[idx];\n            if (leaf.type === 10) {\n                if (keysToRemove.includes(leaf.value[0].value)) {\n                    if (operatorCtx === \"&\") {\n                        newDomain.ast.value.push(...Domain.TRUE.ast.value);\n                    } else if (operatorCtx === \"|\") {\n                        newDomain.ast.value.push(...Domain.FALSE.ast.value);\n                    }\n                } else {\n                    newDomain.ast.value.push(leaf);\n                }\n                return 1;\n            } else if (leaf.type === 1) {\n                // Special case to avoid OR ('|') that can never resolve to true\n                if (\n                    leaf.value === \"|\" &&\n                    elements[idx + 1].type === 10 &&\n                    elements[idx + 2].type === 10 &&\n                    keysToRemove.includes(elements[idx + 1].value[0].value) &&\n                    keysToRemove.includes(elements[idx + 2].value[0].value)\n                ) {\n                    newDomain.ast.value.push(...Domain.TRUE.ast.value);\n                    return 3;\n                }\n                newDomain.ast.value.push(leaf);\n                if (leaf.value === \"!\") {\n                    return 1 + processLeaf(elements, idx + 1, \"&\", newDomain);\n                }\n                const firstLeafSkip = processLeaf(elements, idx + 1, leaf.value, newDomain);\n                const secondLeafSkip = processLeaf(\n                    elements,\n                    idx + 1 + firstLeafSkip,\n                    leaf.value,\n                    newDomain\n                );\n                return 1 + firstLeafSkip + secondLeafSkip;\n            }\n            return 0;\n        }\n\n        domain = new Domain(domain);\n        if (domain.ast.value.length === 0) {\n            return domain;\n        }\n        const newDomain = new Domain([]);\n        processLeaf(domain.ast.value, 0, \"&\", newDomain);\n        return newDomain;\n    }\n\n    /**\n     * @param {DomainRepr} [descr]\n     */\n    constructor(descr = []) {\n        if (descr instanceof Domain) {\n            /** @type {AST} */\n            return new Domain(descr.toString());\n        } else {\n            let rawAST;\n            try {\n                rawAST = typeof descr === \"string\" ? parseExpr(descr) : toAST(descr);\n            } catch (error) {\n                throw new InvalidDomainError(`Invalid domain representation: ${descr.toString()}`, {\n                    cause: error,\n                });\n            }\n            this.ast = normalizeDomainAST(rawAST);\n        }\n    }\n\n    /**\n     * Check if the set of records represented by a domain contains a record\n     *\n     * @param {Object} record\n     * @returns {boolean}\n     */\n    contains(record) {\n        const expr = evaluate(this.ast, record);\n        return matchDomain(record, expr);\n    }\n\n    /**\n     * @returns {string}\n     */\n    toString() {\n        return formatAST(this.ast);\n    }\n\n    /**\n     * @param {Object} context\n     * @returns {DomainListRepr}\n     */\n    toList(context) {\n        return evaluate(this.ast, context);\n    }\n\n    /**\n     * Converts the domain into a human-readable format for JSON representation.\n     * If the domain does not contain any contextual value, it is converted to a list.\n     * Otherwise, it is returned as a string.\n     *\n     * The string format is less readable due to escaped double quotes.\n     * Example: \"[\\\"&\\\",[\\\"user_id\\\",\\\"=\\\",uid],[\\\"team_id\\\",\\\"!=\\\",false]]\"\n     * @returns {DomainListRepr | string}\n     */\n    toJson() {\n        try {\n            // Attempt to evaluate the domain without context\n            const evaluatedAsList = this.toList({});\n            const evaluatedDomain = new Domain(evaluatedAsList);\n            if (evaluatedDomain.toString() === this.toString()) {\n                return evaluatedAsList;\n            }\n            return this.toString();\n        } catch {\n            // The domain couldn't be evaluated due to contextual values\n            return this.toString();\n        }\n    }\n}\n\n/**\n * @param {Array[] | boolean} modifier\n * @param {Object} evalContext\n * @returns {boolean}\n */\nexport function evalDomain(modifier, evalContext) {\n    if (modifier && typeof modifier !== \"boolean\") {\n        modifier = new Domain(modifier).contains(evalContext);\n    }\n    return Boolean(modifier);\n}\n\n/** @type {Condition} */\nconst TRUE_LEAF = [1, \"=\", 1];\n/** @type {Condition} */\nconst FALSE_LEAF = [0, \"=\", 1];\nconst TRUE_DOMAIN = new Domain([TRUE_LEAF]);\nconst FALSE_DOMAIN = new Domain([FALSE_LEAF]);\n\nDomain.TRUE = TRUE_DOMAIN;\nDomain.FALSE = FALSE_DOMAIN;\n\n// -----------------------------------------------------------------------------\n// Helpers\n// -----------------------------------------------------------------------------\n\n/**\n * @param {DomainListRepr} domain\n * @returns {AST}\n */\nfunction toAST(domain) {\n    const elems = domain.map((elem) => {\n        switch (elem) {\n            case \"!\":\n            case \"&\":\n            case \"|\":\n                return { type: 1 /* String */, value: elem };\n            default:\n                return {\n                    type: 10 /* Tuple */,\n                    value: elem.map(toPyValue),\n                };\n        }\n    });\n    return { type: 4 /* List */, value: elems };\n}\n\n/**\n * Normalizes a domain\n *\n * @param {AST} domain\n * @param {'&' | '|'} [op]\n * @returns {AST}\n */\n\nfunction normalizeDomainAST(domain, op = \"&\") {\n    if (domain.type !== 4 /* List */) {\n        if (domain.type === 10 /* Tuple */) {\n            const value = domain.value;\n            /* Tuple contains at least one Tuple and optionally string */\n            if (\n                value.findIndex((e) => e.type === 10) === -1 ||\n                !value.every((e) => e.type === 10 || e.type === 1)\n            ) {\n                throw new InvalidDomainError(\"Invalid domain AST\");\n            }\n        } else {\n            throw new InvalidDomainError(\"Invalid domain AST\");\n        }\n    }\n    if (domain.value.length === 0) {\n        return domain;\n    }\n    let expected = 1;\n    for (const child of domain.value) {\n        switch (child.type) {\n            case 1 /* String */:\n                if (child.value === \"&\" || child.value === \"|\") {\n                    expected++;\n                } else if (child.value !== \"!\") {\n                    throw new InvalidDomainError(\"Invalid domain AST\");\n                }\n                break;\n            case 4: /* list */\n            case 10 /* tuple */:\n                if (child.value.length === 3) {\n                    expected--;\n                    break;\n                }\n                throw new InvalidDomainError(\"Invalid domain AST\");\n            default:\n                throw new InvalidDomainError(\"Invalid domain AST\");\n        }\n    }\n    const values = domain.value.slice();\n    while (expected < 0) {\n        expected++;\n        values.unshift({ type: 1 /* String */, value: op });\n    }\n    if (expected > 0) {\n        throw new InvalidDomainError(\n            `invalid domain ${formatAST(domain)} (missing ${expected} segment(s))`\n        );\n    }\n    return { type: 4 /* List */, value: values };\n}\n\n/**\n * @param {Object} record\n * @param {Condition | boolean} condition\n * @returns {boolean}\n */\nfunction matchCondition(record, condition) {\n    if (typeof condition === \"boolean\") {\n        return condition;\n    }\n    const [field, operator, value] = condition;\n\n    if (typeof field === \"string\") {\n        const names = field.split(\".\");\n        if (names.length >= 2) {\n            return matchCondition(record[names[0]], [names.slice(1).join(\".\"), operator, value]);\n        }\n    }\n    let likeRegexp, ilikeRegexp;\n    if ([\"like\", \"not like\", \"ilike\", \"not ilike\"].includes(operator)) {\n        likeRegexp = new RegExp(`(.*)${escapeRegExp(value).replaceAll(\"%\", \"(.*)\")}(.*)`, \"g\");\n        ilikeRegexp = new RegExp(`(.*)${escapeRegExp(value).replaceAll(\"%\", \"(.*)\")}(.*)`, \"gi\");\n    }\n    const fieldValue = typeof field === \"number\" ? field : record[field];\n    switch (operator) {\n        case \"=?\":\n            if ([false, null].includes(value)) {\n                return true;\n            }\n        // eslint-disable-next-line no-fallthrough\n        case \"=\":\n        case \"==\":\n            if (Array.isArray(fieldValue) && Array.isArray(value)) {\n                return shallowEqual(fieldValue, value);\n            }\n            return fieldValue === value;\n        case \"!=\":\n        case \"<>\":\n            return !matchCondition(record, [field, \"==\", value]);\n        case \"<\":\n            return fieldValue < value;\n        case \"<=\":\n            return fieldValue <= value;\n        case \">\":\n            return fieldValue > value;\n        case \">=\":\n            return fieldValue >= value;\n        case \"in\": {\n            const val = Array.isArray(value) ? value : [value];\n            const fieldVal = Array.isArray(fieldValue) ? fieldValue : [fieldValue];\n            return fieldVal.some((fv) => val.includes(fv));\n        }\n        case \"not in\": {\n            const val = Array.isArray(value) ? value : [value];\n            const fieldVal = Array.isArray(fieldValue) ? fieldValue : [fieldValue];\n            return !fieldVal.some((fv) => val.includes(fv));\n        }\n        case \"like\":\n            if (fieldValue === false) {\n                return false;\n            }\n            return Boolean(fieldValue.match(likeRegexp));\n        case \"not like\":\n            if (fieldValue === false) {\n                return false;\n            }\n            return Boolean(!fieldValue.match(likeRegexp));\n        case \"=like\":\n            if (fieldValue === false) {\n                return false;\n            }\n            return new RegExp(escapeRegExp(value).replace(/%/g, \".*\")).test(fieldValue);\n        case \"ilike\":\n            if (fieldValue === false) {\n                return false;\n            }\n            return Boolean(fieldValue.match(ilikeRegexp));\n        case \"not ilike\":\n            if (fieldValue === false) {\n                return false;\n            }\n            return Boolean(!fieldValue.match(ilikeRegexp));\n        case \"=ilike\":\n            if (fieldValue === false) {\n                return false;\n            }\n            return new RegExp(escapeRegExp(value).replace(/%/g, \".*\"), \"i\").test(fieldValue);\n        case \"any\":\n        case \"not_any\":\n            return true;\n    }\n    throw new InvalidDomainError(\"could not match domain\");\n}\n\n/**\n * @param {Object} record\n * @returns {Object}\n */\nfunction makeOperators(record) {\n    const match = matchCondition.bind(null, record);\n    return {\n        \"!\": (x) => !match(x),\n        \"&\": (a, b) => match(a) && match(b),\n        \"|\": (a, b) => match(a) || match(b),\n    };\n}\n\n/**\n *\n * @param {Object} record\n * @param {DomainListRepr} domain\n * @returns {boolean}\n */\nfunction matchDomain(record, domain) {\n    if (domain.length === 0) {\n        return true;\n    }\n    const operators = makeOperators(record);\n    const reversedDomain = Array.from(domain).reverse();\n    const condStack = [];\n    for (const item of reversedDomain) {\n        const operator = typeof item === \"string\" && operators[item];\n        if (operator) {\n            const operands = condStack.splice(-operator.length);\n            condStack.push(operator(...operands));\n        } else {\n            condStack.push(item);\n        }\n    }\n    return matchCondition(record, condStack.pop());\n}\n", "import { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { Domain } from \"@web/core/domain\";\nimport { TreeEditor } from \"@web/core/tree_editor/tree_editor\";\nimport {\n    domainFromTree,\n    treeFromDomain,\n    formatValue,\n    condition,\n} from \"@web/core/tree_editor/condition_tree\";\nimport { useLoadFieldInfo } from \"@web/core/model_field_selector/utils\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { deepEqual } from \"@web/core/utils/objects\";\nimport { getDomainDisplayedOperators } from \"@web/core/domain_selector/domain_selector_operator_editor\";\nimport { getOperatorEditorInfo } from \"@web/core/tree_editor/tree_editor_operator_editor\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ModelFieldSelector } from \"@web/core/model_field_selector/model_field_selector\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useMakeGetFieldDef } from \"@web/core/tree_editor/utils\";\nimport { getDefaultCondition } from \"./utils\";\n\nconst ARCHIVED_CONDITION = condition(\"active\", \"in\", [true, false]);\nconst ARCHIVED_DOMAIN = `[(\"active\", \"in\", [True, False])]`;\n\nexport class DomainSelector extends Component {\n    static template = \"web.DomainSelector\";\n    static components = { TreeEditor, CheckBox };\n    static props = {\n        domain: String,\n        resModel: String,\n        className: { type: String, optional: true },\n        defaultConnector: { type: [{ value: \"&\" }, { value: \"|\" }], optional: true },\n        isDebugMode: { type: Boolean, optional: true },\n        readonly: { type: Boolean, optional: true },\n        update: { type: Function, optional: true },\n        debugUpdate: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        isDebugMode: false,\n        readonly: true,\n        update: () => {},\n    };\n\n    setup() {\n        this.fieldService = useService(\"field\");\n        this.loadFieldInfo = useLoadFieldInfo(this.fieldService);\n        this.makeGetFieldDef = useMakeGetFieldDef(this.fieldService);\n\n        this.tree = null;\n        this.showArchivedCheckbox = false;\n        this.includeArchived = false;\n\n        onWillStart(() => this.onPropsUpdated(this.props));\n        onWillUpdateProps((np) => this.onPropsUpdated(np));\n    }\n\n    async onPropsUpdated(p) {\n        let domain;\n        let isSupported = true;\n        try {\n            domain = new Domain(p.domain);\n        } catch {\n            isSupported = false;\n        }\n        if (!isSupported) {\n            this.tree = null;\n            this.showArchivedCheckbox = false;\n            this.includeArchived = false;\n            return;\n        }\n\n        const tree = treeFromDomain(domain);\n\n        const getFieldDef = await this.makeGetFieldDef(p.resModel, tree, [\"active\"]);\n\n        this.tree = treeFromDomain(domain, {\n            getFieldDef,\n            distributeNot: !p.isDebugMode,\n        });\n\n        this.showArchivedCheckbox = this.getShowArchivedCheckBox(Boolean(getFieldDef(\"active\")), p);\n        this.includeArchived = false;\n        if (this.showArchivedCheckbox) {\n            if (this.tree.value === \"&\") {\n                this.tree.children = this.tree.children.filter((child) => {\n                    if (deepEqual(child, ARCHIVED_CONDITION)) {\n                        this.includeArchived = true;\n                        return false;\n                    }\n                    return true;\n                });\n                if (this.tree.children.length === 1) {\n                    this.tree = this.tree.children[0];\n                }\n            } else if (deepEqual(this.tree, ARCHIVED_CONDITION)) {\n                this.includeArchived = true;\n                this.tree = treeFromDomain(`[]`);\n            }\n        }\n    }\n\n    getShowArchivedCheckBox(hasActiveField, props) {\n        return hasActiveField;\n    }\n\n    getDefaultCondition(fieldDefs) {\n        return getDefaultCondition(fieldDefs);\n    }\n\n    getDefaultOperator(fieldDef) {\n        return getDomainDisplayedOperators(fieldDef)[0];\n    }\n\n    getOperatorEditorInfo(fieldDef) {\n        const operators = getDomainDisplayedOperators(fieldDef);\n        return getOperatorEditorInfo(operators, fieldDef);\n    }\n\n    getPathEditorInfo(resModel, defaultCondition) {\n        const { isDebugMode } = this.props;\n        return {\n            component: ModelFieldSelector,\n            extractProps: ({ update, value: path }) => {\n                return {\n                    path,\n                    update,\n                    resModel,\n                    isDebugMode,\n                    readonly: false,\n                };\n            },\n            isSupported: (path) => [0, 1].includes(path) || typeof path === \"string\",\n            defaultValue: () => defaultCondition.path,\n            stringify: (path) => formatValue(path),\n            message: _t(\"Invalid field chain\"),\n        };\n    }\n\n    toggleIncludeArchived() {\n        this.includeArchived = !this.includeArchived;\n        this.update(this.tree);\n    }\n\n    resetDomain() {\n        this.props.update(\"[]\");\n    }\n\n    onDomainInput(domain) {\n        if (this.props.debugUpdate) {\n            this.props.debugUpdate(domain);\n        }\n    }\n\n    onDomainChange(domain) {\n        this.props.update(domain, true);\n    }\n    update(tree) {\n        const archiveDomain = this.includeArchived ? ARCHIVED_DOMAIN : `[]`;\n        const domain = tree\n            ? Domain.and([domainFromTree(tree), archiveDomain]).toString()\n            : archiveDomain;\n        this.props.update(domain);\n    }\n}\n", "export function getDomainDisplayedOperators(fieldDef) {\n    if (!fieldDef) {\n        fieldDef = {};\n    }\n    const { type, is_property } = fieldDef;\n\n    if (is_property) {\n        switch (type) {\n            case \"many2many\":\n            case \"tags\":\n                return [\"in\", \"not in\", \"set\", \"not_set\"];\n            case \"many2one\":\n            case \"selection\":\n                return [\"=\", \"!=\", \"set\", \"not_set\"];\n        }\n    }\n\n    switch (type) {\n        case \"boolean\":\n            return [\"is\", \"is_not\"];\n        case \"selection\":\n            return [\"=\", \"!=\", \"in\", \"not in\", \"set\", \"not_set\"];\n        case \"char\":\n        case \"text\":\n        case \"html\":\n            return [\n                \"=\",\n                \"!=\",\n                \"ilike\",\n                \"not ilike\",\n                \"in\",\n                \"not in\",\n                \"set\",\n                \"not_set\",\n                \"starts_with\",\n                \"ends_with\",\n            ];\n        case \"date\":\n        case \"datetime\":\n            return [\"=\", \"!=\", \">\", \">=\", \"<\", \"<=\", \"between\", \"within\", \"set\", \"not_set\"];\n        case \"integer\":\n        case \"float\":\n        case \"monetary\":\n            return [\n                \"=\",\n                \"!=\",\n                \">\",\n                \">=\",\n                \"<\",\n                \"<=\",\n                \"between\",\n                \"ilike\",\n                \"not ilike\",\n                \"set\",\n                \"not_set\",\n            ];\n        case \"many2one\":\n        case \"many2many\":\n        case \"one2many\":\n            return [\n                \"in\",\n                \"not in\",\n                \"=\",\n                \"!=\",\n                \"ilike\",\n                \"not ilike\",\n                \"set\",\n                \"not_set\",\n                \"starts_with\",\n                \"ends_with\",\n                \"any\",\n                \"not any\",\n            ];\n        case \"json\":\n            return [\"=\", \"!=\", \"ilike\", \"not ilike\", \"set\", \"not_set\"];\n        case \"properties\":\n            return [\"set\", \"not_set\"];\n        case undefined:\n            return [\"=\"];\n        default:\n            return [\n                \"=\",\n                \"!=\",\n                \">\",\n                \">=\",\n                \"<\",\n                \"<=\",\n                \"ilike\",\n                \"not ilike\",\n                \"like\",\n                \"not like\",\n                \"=like\",\n                \"=ilike\",\n                \"child_of\",\n                \"parent_of\",\n                \"in\",\n                \"not in\",\n                \"set\",\n                \"not_set\",\n            ];\n    }\n}\n", "import { getDefaultValue } from \"@web/core/tree_editor/tree_editor_value_editors\";\nimport { getDomainDisplayedOperators } from \"@web/core/domain_selector/domain_selector_operator_editor\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { domainFromTree, condition } from \"@web/core/tree_editor/condition_tree\";\nimport { getDefaultPath } from \"@web/core/tree_editor/utils\";\n\nexport function getDefaultCondition(fieldDefs) {\n    const defaultPath = getDefaultPath(fieldDefs);\n    const fieldDef = fieldDefs[defaultPath];\n    const operator = getDomainDisplayedOperators(fieldDef)[0];\n    const value = getDefaultValue(fieldDef, operator);\n    return condition(fieldDef.name, operator, value);\n}\n\nexport function getDefaultDomain(fieldDefs) {\n    return domainFromTree(getDefaultCondition(fieldDefs));\n}\n\nexport function useGetDefaultLeafDomain() {\n    const fieldService = useService(\"field\");\n    return async (resModel) => {\n        const fieldDefs = await fieldService.loadFields(resModel);\n        return getDefaultDomain(fieldDefs);\n    };\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, useRef, useState } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Domain } from \"@web/core/domain\";\nimport { DomainSelector } from \"@web/core/domain_selector/domain_selector\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { user } from \"@web/core/user\";\n\nexport class DomainSelectorDialog extends Component {\n    static template = \"web.DomainSelectorDialog\";\n    static components = {\n        Dialog,\n        DomainSelector,\n    };\n    static props = {\n        close: Function,\n        onConfirm: Function,\n        resModel: String,\n        className: { type: String, optional: true },\n        defaultConnector: { type: [{ value: \"&\" }, { value: \"|\" }], optional: true },\n        domain: String,\n        isDebugMode: { type: Boolean, optional: true },\n        readonly: { type: Boolean, optional: true },\n        text: { type: String, optional: true },\n        confirmButtonText: { type: String, optional: true },\n        disableConfirmButton: { type: Function, optional: true },\n        discardButtonText: { type: String, optional: true },\n        title: { type: String, optional: true },\n        context: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        isDebugMode: false,\n        readonly: false,\n        context: {},\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.state = useState({ domain: this.props.domain });\n        this.confirmButtonRef = useRef(\"confirm\");\n    }\n\n    get confirmButtonText() {\n        return this.props.confirmButtonText || _t(\"Confirm\");\n    }\n\n    get dialogTitle() {\n        return this.props.title || _t(\"Domain\");\n    }\n\n    get disabled() {\n        if (this.props.disableConfirmButton) {\n            return this.props.disableConfirmButton(this.state.domain);\n        }\n        return false;\n    }\n\n    get discardButtonText() {\n        return this.props.discardButtonText || _t(\"Discard\");\n    }\n\n    get domainSelectorProps() {\n        return {\n            className: this.props.className,\n            resModel: this.props.resModel,\n            readonly: this.props.readonly,\n            isDebugMode: this.props.isDebugMode,\n            defaultConnector: this.props.defaultConnector,\n            domain: this.state.domain,\n            update: (domain) => {\n                this.state.domain = domain;\n            },\n        };\n    }\n\n    async onConfirm() {\n        this.confirmButtonRef.el.disabled = true;\n        let domain;\n        let isValid;\n        try {\n            const evalContext = { ...user.context, ...this.props.context };\n            domain = new Domain(this.state.domain).toList(evalContext);\n        } catch {\n            isValid = false;\n        }\n        if (isValid === undefined) {\n            isValid = await rpc(\"/web/domain/validate\", {\n                model: this.props.resModel,\n                domain,\n            });\n        }\n        if (!isValid) {\n            if (this.confirmButtonRef.el) {\n                this.confirmButtonRef.el.disabled = false;\n            }\n            this.notification.add(_t(\"Domain is invalid. Please correct it\"), {\n                type: \"danger\",\n            });\n            return;\n        }\n        this.props.onConfirm(this.state.domain);\n        this.props.close();\n    }\n\n    onDiscard() {\n        this.props.close();\n    }\n}\n", "import { useComponent, useEffect, useEnv } from \"@odoo/owl\";\nimport { DROPDOWN_GROUP } from \"@web/core/dropdown/dropdown_group\";\n\n/**\n * @typedef DropdownGroupState\n * @property {boolean} isInGroup\n * @property {boolean} isOpen\n */\n\n/**\n * Will add (and remove) a dropdown from a parent\n * DropdownGroup component, allowing it to know\n * if it's in a group and if the group is open.\n *\n * @returns {DropdownGroupState}\n */\nexport function useDropdownGroup() {\n    const env = useEnv();\n\n    const group = {\n        isInGroup: DROPDOWN_GROUP in env,\n        get isOpen() {\n            return this.isInGroup && [...env[DROPDOWN_GROUP]].some((dropdown) => dropdown.isOpen);\n        },\n    };\n\n    if (group.isInGroup) {\n        const dropdown = useComponent();\n        useEffect(() => {\n            env[DROPDOWN_GROUP].add(dropdown.state);\n            return () => env[DROPDOWN_GROUP].delete(dropdown.state);\n        });\n    }\n\n    return group;\n}\n", "import { EventBus, onWillDestroy, useChildSubEnv, useEffect, useEnv } from \"@odoo/owl\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { effect } from \"@web/core/utils/reactive\";\n\nexport const DROPDOWN_NESTING = Symbol(\"dropdownNesting\");\nconst BUS = new EventBus();\n\nclass DropdownNestingState {\n    constructor({ parent, close }) {\n        this._isOpen = false;\n        this.parent = parent;\n        this.children = new Set();\n        this.close = close;\n\n        parent?.children.add(this);\n    }\n\n    set isOpen(value) {\n        this._isOpen = value;\n        if (this._isOpen) {\n            BUS.trigger(\"dropdown-opened\", this);\n        }\n    }\n\n    get isOpen() {\n        return this._isOpen;\n    }\n\n    remove() {\n        this.parent?.children.delete(this);\n    }\n\n    closeAllParents() {\n        this.close();\n        if (this.parent) {\n            this.parent.closeAllParents();\n        }\n    }\n\n    closeChildren() {\n        this.children.forEach((child) => child.close());\n    }\n\n    shouldIgnoreChanges(other) {\n        return (\n            other === this ||\n            other.activeEl !== this.activeEl ||\n            [...this.children].some((child) => child.shouldIgnoreChanges(other))\n        );\n    }\n\n    handleChange(other) {\n        // Prevents closing the dropdown when a change is coming from itself or from a children.\n        if (this.shouldIgnoreChanges(other)) {\n            return;\n        }\n\n        if (other.isOpen && this.isOpen) {\n            this.close();\n        }\n    }\n}\n\n/**\n * This hook is used to manage communication between dropdowns.\n *\n * When a dropdown is open, every other dropdown that is not a parent\n * is closed. It also uses the current's ui active element to only\n * close itself when the active element is the same as the current\n * dropdown to separate dropdowns in different dialogs.\n *\n * @param {import(\"@web/core/dropdown/dropdown\").DropdownState} state\n * @returns\n */\nexport function useDropdownNesting(state) {\n    const env = useEnv();\n    const current = new DropdownNestingState({\n        parent: env[DROPDOWN_NESTING],\n        close: () => state.close(),\n    });\n\n    // Set up UI active element related behavior ---------------------------\n    const uiService = useService(\"ui\");\n    useEffect(\n        () => {\n            Promise.resolve().then(() => {\n                current.activeEl = uiService.activeElement;\n            });\n        },\n        () => []\n    );\n\n    useChildSubEnv({ [DROPDOWN_NESTING]: current });\n    useBus(BUS, \"dropdown-opened\", ({ detail: other }) => current.handleChange(other));\n\n    effect(\n        (state) => {\n            current.isOpen = state.isOpen;\n        },\n        [state]\n    );\n\n    onWillDestroy(() => {\n        current.remove();\n    });\n\n    return {\n        get hasParent() {\n            return Boolean(current.parent);\n        },\n        /**@type {import(\"@web/core/navigation/navigation\").NavigationOptions} */\n        navigationOptions: {\n            onEnabled: (items) => {\n                if (current.parent) {\n                    items[0]?.focus();\n                }\n            },\n            onMouseEnter: (item) => {\n                if (item.target.classList.contains(\"o-dropdown\")) {\n                    item.select();\n                }\n            },\n            hotkeys: {\n                escape: () => current.close(),\n                arrowleft: (index, items) => {\n                    if (\n                        localization.direction === \"rtl\" &&\n                        items[index]?.target.classList.contains(\"o-dropdown\")\n                    ) {\n                        items[index]?.select();\n                    } else if (current.parent) {\n                        current.close();\n                    }\n                },\n                arrowright: (index, items) => {\n                    if (localization.direction === \"rtl\" && current.parent) {\n                        current.close();\n                    } else if (items[index]?.target.classList.contains(\"o-dropdown\")) {\n                        items[index]?.select();\n                    }\n                },\n            },\n        },\n    };\n}\n", "import { Component, onMounted, onRendered, onWillDestroy, onWillStart, xml } from \"@odoo/owl\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nexport class DropdownPopover extends Component {\n    static components = { DropdownItem };\n    static template = xml`\n        <t t-if=\"this.props.items\">\n            <t t-foreach=\"this.props.items\" t-as=\"item\" t-key=\"this.getKey(item, item_index)\">\n                <DropdownItem class=\"item.class\" onSelected=\"() => item.onSelected()\" t-out=\"item.label\"/>\n            </t>\n        </t>\n        <t t-slot=\"content\" />\n    `;\n    static props = {\n        // Popover service\n        close: { type: Function, optional: true },\n\n        // Events & Handlers\n        beforeOpen: { type: Function, optional: true },\n        onOpened: { type: Function, optional: true },\n        onClosed: { type: Function, optional: true },\n\n        // Rendering & Context\n        refresher: Object,\n        slots: Object,\n        items: { type: Array, optional: true },\n    };\n\n    setup() {\n        onRendered(() => {\n            // Note that the Dropdown component and the DropdownPopover component\n            // are not in the same context.\n            // So when the Dropdown component is re-rendered, the DropdownPopover\n            // component must also re-render itself.\n            // This is why we subscribe to this reactive, which is changed when\n            // the Dropdown component is re-rendered.\n            this.props.refresher.token;\n        });\n\n        onWillStart(async () => {\n            await this.props.beforeOpen?.();\n        });\n\n        onMounted(() => {\n            this.props.onOpened?.();\n        });\n\n        onWillDestroy(() => {\n            this.props.onClosed?.();\n        });\n    }\n\n    getKey(item, index) {\n        return \"id\" in item ? item.id : index;\n    }\n}\n", "import { Component, onPatched, useState } from \"@odoo/owl\";\n\nexport const ACCORDION = Symbol(\"Accordion\");\nexport class AccordionItem extends Component {\n    static template = \"web.AccordionItem\";\n    static components = {};\n    static props = {\n        slots: {\n            type: Object,\n            shape: {\n                default: {},\n            },\n        },\n        description: String,\n        selected: {\n            type: Boolean,\n            optional: true,\n        },\n        class: {\n            type: String,\n            optional: true,\n        },\n    };\n    static defaultProps = {\n        class: \"\",\n        selected: false,\n    };\n\n    setup() {\n        this.state = useState({\n            open: false,\n        });\n        this.parentComponent = this.env[ACCORDION];\n        onPatched(() => {\n            this.parentComponent?.accordionStateChanged?.();\n        });\n    }\n}\n", "import { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nexport class CheckboxItem extends DropdownItem {\n    static template = \"web.CheckboxItem\";\n    static props = {\n        ...DropdownItem.props,\n        checked: {\n            type: Boolean,\n            optional: false,\n        },\n    };\n}\n", "import {\n    Component,\n    onMounted,\n    onRendered,\n    onWillUpdateProps,\n    reactive,\n    status,\n    useEffect,\n    xml,\n} from \"@odoo/owl\";\nimport { useDropdownGroup } from \"@web/core/dropdown/_behaviours/dropdown_group_hook\";\nimport { useDropdownNesting } from \"@web/core/dropdown/_behaviours/dropdown_nesting\";\nimport { DropdownPopover } from \"@web/core/dropdown/_behaviours/dropdown_popover\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { useNavigation } from \"@web/core/navigation/navigation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { mergeClasses } from \"@web/core/utils/classname\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { deepMerge } from \"@web/core/utils/objects\";\nimport { effect } from \"@web/core/utils/reactive\";\n\nfunction getFirstElementOfNode(node) {\n    if (!node) {\n        return null;\n    }\n    if (node.el) {\n        return node.el.nodeType === Node.ELEMENT_NODE ? node.el : null;\n    }\n    if (node.bdom || node.child) {\n        return getFirstElementOfNode(node.bdom || node.child);\n    }\n    if (node.children) {\n        for (const child of node.children) {\n            const el = getFirstElementOfNode(child);\n            if (el) {\n                return el;\n            }\n        }\n    }\n    return null;\n}\n\n/**\n * The Dropdown component allows to define a menu that will\n * show itself when a target is toggled.\n *\n * Items are defined using DropdownItems. Dropdowns are\n * also allowed as items to be able to create nested\n * dropdown menus.\n */\nexport class Dropdown extends Component {\n    static template = xml`<t t-slot=\"default\"/>`;\n    static components = {};\n    static props = {\n        arrow: { optional: true },\n        menuClass: { optional: true },\n        position: { type: String, optional: true },\n        slots: {\n            type: Object,\n            shape: {\n                default: { optional: true },\n                content: { optional: true },\n            },\n        },\n\n        items: {\n            optional: true,\n            type: Array,\n            elements: {\n                type: Object,\n                shape: {\n                    label: String,\n                    onSelected: Function,\n                    class: { optional: true },\n                    \"*\": true,\n                },\n            },\n        },\n\n        menuRef: { type: Function, optional: true }, // to be used with useChildRef\n        disabled: { type: Boolean, optional: true },\n        holdOnHover: { type: Boolean, optional: true },\n\n        beforeOpen: { type: Function, optional: true },\n        onOpened: { type: Function, optional: true },\n        onStateChanged: { type: Function, optional: true },\n\n        /** Manual state handling, @see useDropdownState */\n        state: {\n            type: Object,\n            shape: {\n                isOpen: Boolean,\n                close: Function,\n                open: Function,\n                \"*\": true,\n            },\n            optional: true,\n        },\n        manual: { type: Boolean, optional: true },\n\n        /**\n         * Override the internal navigation hook options\n         * @type {import(\"@web/core/navigation/navigation\").NavigationOptions}\n         */\n        navigationOptions: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        arrow: false,\n        disabled: false,\n        holdOnHover: false,\n        menuClass: \"\",\n        state: undefined,\n        navigationOptions: {},\n    };\n\n    setup() {\n        this.menuRef = this.props.menuRef || useChildRef();\n\n        this.state = this.props.state || useDropdownState();\n        this.nesting = useDropdownNesting(this.state);\n        this.group = useDropdownGroup();\n        this.navigation = useNavigation(this.menuRef, {\n            focusInitialElementOnDisabled: () => !this.group.isInGroup,\n            itemsSelector: \":scope .o-navigable, :scope .o-dropdown\",\n            // Using deepMerge allows to keep entries of both option.hotkeys\n            ...deepMerge(this.nesting.navigationOptions, this.props.navigationOptions),\n        });\n\n        // Set up UI active element related behavior ---------------------------\n        let activeEl;\n        this.uiService = useService(\"ui\");\n        useEffect(\n            () => {\n                Promise.resolve().then(() => {\n                    activeEl = this.uiService.activeElement;\n                });\n            },\n            () => []\n        );\n\n        this.popover = usePopover(DropdownPopover, {\n            animation: false,\n            arrow: this.props.arrow,\n            closeOnClickAway: (target) => {\n                return this.popoverCloseOnClickAway(target, activeEl);\n            },\n            closeOnEscape: false, // Handled via navigation and prevents closing root of nested dropdown\n            env: this.__owl__.childEnv,\n            holdOnHover: this.props.holdOnHover,\n            onClose: () => this.state.close(),\n            onPositioned: (el, { direction }) => this.setTargetDirectionClass(direction),\n            popoverClass: mergeClasses(\n                \"o-dropdown--menu dropdown-menu mx-0\",\n                { \"o-dropdown--menu-submenu\": this.hasParent },\n                this.props.menuClass\n            ),\n            popoverRole: \"menu\",\n            position: this.position,\n            ref: this.menuRef,\n            setActiveElement: false,\n        });\n\n        // As the popover is in another context we need to force\n        // its re-rendering when the dropdown re-renders\n        onRendered(() => (this.popoverRefresher ? this.popoverRefresher.token++ : null));\n\n        onMounted(() => this.onStateChanged(this.state));\n        effect((state) => this.onStateChanged(state), [this.state]);\n\n        useEffect(\n            (target) => this.setTargetElement(target),\n            () => [this.target]\n        );\n\n        onWillUpdateProps(({ disabled }) => {\n            if (disabled) {\n                this.closePopover();\n            }\n        });\n    }\n\n    /** @type {string} */\n    get position() {\n        return this.props.position || (this.hasParent ? \"right-start\" : \"bottom-start\");\n    }\n\n    get hasParent() {\n        return this.nesting.hasParent;\n    }\n\n    /** @type {HTMLElement|null} */\n    get target() {\n        const target = getFirstElementOfNode(this.__owl__.bdom);\n        if (!target) {\n            throw new Error(\n                \"Could not find a valid dropdown toggler, prefer a single html element and put any dynamic content inside of it.\"\n            );\n        }\n        return target;\n    }\n\n    handleClick(event) {\n        if (this.props.disabled) {\n            return;\n        }\n\n        event.stopPropagation();\n        if (this.state.isOpen && !this.hasParent) {\n            this.state.close();\n        } else {\n            this.state.open();\n        }\n    }\n\n    handleMouseEnter() {\n        if (this.props.disabled) {\n            return;\n        }\n\n        if (this.hasParent || this.group.isOpen) {\n            this.target.focus();\n            this.state.open();\n        }\n    }\n\n    onStateChanged(state) {\n        if (state.isOpen) {\n            this.openPopover();\n        } else {\n            this.closePopover();\n        }\n    }\n\n    popoverCloseOnClickAway(target, activeEl) {\n        return this.uiService.getActiveElementOf(target) === activeEl;\n    }\n\n    setTargetElement(target) {\n        if (!target) {\n            return;\n        }\n\n        target.ariaExpanded = false;\n        target.classList.add(\"o-dropdown\");\n\n        if (this.hasParent) {\n            target.classList.add(\"o-dropdown--has-parent\");\n        }\n\n        const tagName = target.tagName.toLowerCase();\n        if (![\"input\", \"textarea\", \"table\", \"thead\", \"tbody\", \"tr\", \"th\", \"td\"].includes(tagName)) {\n            target.classList.add(\"dropdown-toggle\");\n            if (this.hasParent) {\n                target.classList.add(\"o-dropdown-item\", \"o-navigable\", \"dropdown-item\");\n\n                if (!target.classList.contains(\"o-dropdown--no-caret\")) {\n                    target.classList.add(\"o-dropdown-caret\");\n                }\n            }\n        }\n\n        this.defaultDirection = this.position.split(\"-\")[0];\n        this.setTargetDirectionClass(this.defaultDirection);\n\n        if (!this.props.manual) {\n            target.addEventListener(\"click\", this.handleClick.bind(this));\n            target.addEventListener(\"mouseenter\", this.handleMouseEnter.bind(this));\n\n            return () => {\n                target.removeEventListener(\"click\", this.handleClick.bind(this));\n                target.removeEventListener(\"mouseenter\", this.handleMouseEnter.bind(this));\n            };\n        }\n    }\n\n    setTargetDirectionClass(direction) {\n        if (!this.target) {\n            return;\n        }\n        const directionClasses = {\n            bottom: \"dropdown\",\n            top: \"dropup\",\n            left: \"dropstart\",\n            right: \"dropend\",\n        };\n        this.target.classList.remove(...Object.values(directionClasses));\n        this.target.classList.add(directionClasses[direction]);\n    }\n\n    openPopover() {\n        if (this.popover.isOpen || status(this) !== \"mounted\") {\n            return;\n        }\n        if (!this.target || !this.target.isConnected) {\n            this.state.close();\n            return;\n        }\n\n        this.popoverRefresher = reactive({ token: 0 });\n        const props = {\n            beforeOpen: () => this.props.beforeOpen?.(),\n            onOpened: () => this.onOpened(),\n            onClosed: () => this.onClosed(),\n            refresher: this.popoverRefresher,\n            items: this.props.items,\n            slots: this.props.slots,\n        };\n        this.popover.open(this.target, props);\n    }\n\n    closePopover() {\n        this.popover.close();\n        this.navigation.disable();\n    }\n\n    onOpened() {\n        this.navigation.enable();\n        this.props.onOpened?.();\n        this.props.onStateChanged?.(true);\n\n        if (this.target) {\n            this.target.ariaExpanded = true;\n            this.target.classList.add(\"show\");\n        }\n    }\n\n    onClosed() {\n        this.props.onStateChanged?.(false);\n\n        if (this.target) {\n            this.target.ariaExpanded = false;\n            this.target.classList.remove(\"show\");\n            this.setTargetDirectionClass(this.defaultDirection);\n        }\n    }\n}\n", "import { Component, onWillDestroy, useChildSubEnv, xml } from \"@odoo/owl\";\n\nconst GROUPS = new Map();\n\nfunction getGroup(id) {\n    if (!GROUPS.has(id)) {\n        GROUPS.set(id, {\n            group: new Set(),\n            count: 0,\n        });\n    }\n    GROUPS.get(id).count++;\n    return GROUPS.get(id).group;\n}\n\nfunction removeGroup(id) {\n    const groupData = GROUPS.get(id);\n    groupData.count--;\n    if (groupData.count <= 0) {\n        GROUPS.delete(id);\n    }\n}\n\nexport const DROPDOWN_GROUP = Symbol(\"dropdownGroup\");\nexport class DropdownGroup extends Component {\n    static template = xml`<t t-slot=\"default\"/>`;\n    static props = {\n        group: { type: String, optional: true },\n        slots: Object,\n    };\n\n    setup() {\n        if (this.props.group) {\n            const group = getGroup(this.props.group);\n            onWillDestroy(() => removeGroup(this.props.group));\n            useChildSubEnv({ [DROPDOWN_GROUP]: group });\n        } else {\n            useChildSubEnv({ [DROPDOWN_GROUP]: new Set() });\n        }\n    }\n}\n", "import { useEnv, useState } from \"@odoo/owl\";\nimport { DROPDOWN_NESTING } from \"@web/core/dropdown/_behaviours/dropdown_nesting\";\n\n/**\n * @typedef {Object} DropdownState\n * @property {() => void} open\n * @property {() => void} close\n * @property {boolean} isOpen\n */\n\n/**\n * Hook used to interact with the Dropdown state.\n * In order to use it, pass the returned state to the dropdown component, i.e.:\n *  <Dropdown state=\"dropdownState\" ...>...</Dropdown>\n * @param {Object} callbacks\n * @param {Function} callbacks.onOpen\n * @param {Function} callbacks.onClose\n * @returns {DropdownState}\n */\nexport function useDropdownState({ onOpen, onClose } = {}) {\n    const state = useState({\n        isOpen: false,\n        open: () => {\n            state.isOpen = true;\n            onOpen?.();\n        },\n        close: () => {\n            state.isOpen = false;\n            onClose?.();\n        },\n    });\n    return state;\n}\n\n/**\n * Can be used by components to have some control\n * how and when a wrapping dropdown should close.\n */\nexport function useDropdownCloser() {\n    const env = useEnv();\n    const dropdown = env[DROPDOWN_NESTING];\n    return {\n        close: () => dropdown?.close(),\n        closeChildren: () => dropdown?.closeChildren(),\n        closeAll: () => dropdown?.closeAllParents(),\n    };\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useDropdownCloser } from \"@web/core/dropdown/dropdown_hooks\";\n\nconst ClosingMode = {\n    None: \"none\",\n    ClosestParent: \"closest\",\n    AllParents: \"all\",\n};\n\nexport class DropdownItem extends Component {\n    static template = \"web.DropdownItem\";\n    static props = {\n        class: {\n            type: [String, Object],\n            optional: true,\n        },\n        onSelected: {\n            type: Function,\n            optional: true,\n        },\n        closingMode: {\n            type: ClosingMode,\n            optional: true,\n        },\n        attrs: {\n            type: Object,\n            optional: true,\n        },\n        slots: { Object, optional: true },\n    };\n    static defaultProps = {\n        closingMode: ClosingMode.AllParents,\n        attrs: {},\n    };\n\n    setup() {\n        this.dropdownControl = useDropdownCloser();\n    }\n\n    onClick(ev) {\n        if (this.props.attrs && this.props.attrs.href) {\n            ev.preventDefault();\n        }\n        this.props.onSelected?.();\n        switch (this.props.closingMode) {\n            case ClosingMode.ClosestParent:\n                this.dropdownControl.close();\n                break;\n            case ClosingMode.AllParents:\n                this.dropdownControl.closeAll();\n                break;\n        }\n    }\n}\n", "import { Component, useEffect, useRef, useState } from \"@odoo/owl\";\n\nexport class Dropzone extends Component {\n    static props = {\n        extraClass: { type: String, optional: true },\n        onDrop: { type: Function, optional: true },\n        ref: Object,\n    };\n    static template = \"web.Dropzone\";\n\n    setup() {\n        super.setup();\n        this.root = useRef(\"root\");\n        this.state = useState({\n            isDraggingInside: false,\n        });\n        useEffect(() => {\n            const { top, left, width, height } = this.props.ref.el.getBoundingClientRect();\n            this.root.el.style = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`;\n        });\n    }\n}\n", "import { Dropzone } from \"@web/core/dropzone/dropzone\";\n\nimport { useEffect, useExternalListener } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\n\nconst componentRegistry = registry.category(\"main_components\");\n\nlet id = 1;\nexport function useDropzone(targetRef, onDrop, extraClass, isDropzoneEnabled = () => true) {\n    const dropzoneId = `web.dropzone_${id++}`;\n    let dragCount = 0;\n    let hasTarget = false;\n\n    useExternalListener(document, \"dragenter\", onDragEnter);\n    useExternalListener(document, \"dragleave\", onDragLeave);\n    // Prevents the browser to open or download the file when it is dropped\n    // outside of the dropzone.\n    useExternalListener(window, \"dragover\", (ev) => ev.preventDefault());\n    useExternalListener(window, \"drop\", (ev) => {\n        ev.preventDefault();\n        dragCount = 0;\n        updateDropzone();\n    });\n\n    function updateDropzone() {\n        const shouldDisplayDropzone = dragCount && hasTarget && isDropzoneEnabled();\n        const hasDropzone = componentRegistry.contains(dropzoneId);\n        if (shouldDisplayDropzone && !hasDropzone) {\n            componentRegistry.add(dropzoneId, {\n                Component: Dropzone,\n                props: { extraClass, onDrop, ref: targetRef },\n            });\n        }\n        if (!shouldDisplayDropzone && hasDropzone) {\n            componentRegistry.remove(dropzoneId);\n        }\n    }\n\n    function onDragEnter(ev) {\n        if (dragCount || (ev.dataTransfer && ev.dataTransfer.types.includes(\"Files\"))) {\n            dragCount++;\n            updateDropzone();\n        }\n    }\n\n    function onDragLeave() {\n        if (dragCount) {\n            dragCount--;\n            updateDropzone();\n        }\n    }\n\n    useEffect(\n        (el) => {\n            hasTarget = !!el;\n            updateDropzone();\n        },\n        () => [targetRef.el]\n    );\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { RainbowMan } from \"./rainbow_man\";\n\nconst effectRegistry = registry.category(\"effects\");\n\n// -----------------------------------------------------------------------------\n// RainbowMan effect\n// -----------------------------------------------------------------------------\n\n/**\n * Handles effect of type \"rainbow_man\". If the effects aren't disabled, returns\n * the RainbowMan component to instantiate and its props. If the effects are\n * disabled, displays the message in a notification.\n *\n * @param {Object} env\n * @param {Object} [params={}]\n * @param {string} [params.message=\"Well Done!\"]\n *    The message in the notice the rainbowman holds or the content of the notification if effects are disabled\n *    Can be a simple a string\n *    Can be a string representation of html (prefer component if you want interactions in the DOM)\n * @param {string} [params.img_url=\"/web/static/img/smile.svg\"]\n *    The url of the image to display inside the rainbow\n * @param {\"slow\"|\"medium\"|\"fast\"|\"no\"} [params.fadeout=\"medium\"]\n *    Delay for rainbowman to disappear\n *    'fast' will make rainbowman dissapear quickly\n *    'medium' and 'slow' will wait little longer before disappearing (can be used when options.message is longer)\n *    'no' will keep rainbowman on screen until user clicks anywhere outside rainbowman\n * @param {typeof import(\"@odoo/owl\").Component} [params.Component]\n *    Custom Component class to instantiate inside the Rainbow Man\n * @param {Object} [params.props]\n *    If params.Component is given, its props can be passed with this argument\n */\nfunction rainbowMan(env, params = {}) {\n    let message = params.message;\n    if (message instanceof Element) {\n        console.log(\n            \"Providing an HTML element to an effect is deprecated. Note that all event handlers will be lost.\"\n        );\n        message = message.outerHTML;\n    } else if (!message) {\n        message = _t(\"Well Done!\");\n    }\n    if (user.showEffect) {\n        /** @type {import(\"./rainbow_man\").RainbowManProps} */\n        const props = {\n            imgUrl: params.img_url || \"/web/static/img/smile.svg\",\n            fadeout: params.fadeout || \"medium\",\n            message,\n            Component: params.Component,\n            props: params.props,\n        };\n        return { Component: RainbowMan, props };\n    }\n    env.services.notification.add(message);\n}\neffectRegistry.add(\"rainbow_man\", rainbowMan);\n\n// -----------------------------------------------------------------------------\n// Effect service\n// -----------------------------------------------------------------------------\n\nexport const effectService = {\n    dependencies: [\"overlay\"],\n    start(env, { overlay }) {\n        /**\n         * @param {Object} [params] various params depending on the type of effect\n         * @param {string} [params.type=\"rainbow_man\"] the effect to display\n         */\n        const add = (params = {}) => {\n            const type = params.type || \"rainbow_man\";\n            const effect = effectRegistry.get(type);\n            const { Component, props } = effect(env, params) || {};\n            if (Component) {\n                const remove = overlay.add(Component, {\n                    ...props,\n                    close: () => remove(),\n                });\n            }\n        };\n\n        return { add };\n    },\n};\n\nregistry.category(\"services\").add(\"effect\", effectService);\n", "import { browser } from \"@web/core/browser/browser\";\n\nimport { Component, useEffect, useExternalListener, useState } from \"@odoo/owl\";\n\n/**\n * @typedef Common\n * @property {string} [fadeout='medium'] Delay for rainbowman to disappear.\n *  - 'fast' will make rainbowman dissapear quickly,\n *  - 'medium' and 'slow' will wait little longer before disappearing\n *      (can be used when props.message is longer),\n *  - 'no' will keep rainbowman on screen until user clicks anywhere outside rainbowman\n * @property {string} [imgUrl] URL of the image to be displayed\n *\n * @typedef Simple\n * @property {string} message Message to be displayed on rainbowman card\n *\n * @typedef Custom\n * @property {typeof import(\"@odoo/owl\").Component} Component\n * @property {any} [props]\n *\n * @typedef {Common & (Simple | Custom)} RainbowManProps\n */\n\n/**\n * The RainbowMan Component is meant to display a 'fun/rewarding' message.  For\n * example, when the user marked a large deal as won, or when he cleared its inbox.\n *\n * This component is mostly a picture and a message with a rainbow animation around.\n * If you want to display a RainbowMan, you probably do not want to do it by\n * importing this file.  The usual way to do that would be to use the effect\n * service.\n */\nexport class RainbowMan extends Component {\n    static template = \"web.RainbowMan\";\n    static rainbowFadeouts = { slow: 4500, medium: 3500, fast: 2000, no: false };\n    static props = {\n        fadeout: String,\n        close: Function,\n        message: String,\n        imgUrl: String,\n        Component: { type: Function, optional: true },\n        props: { type: Object, optional: true },\n    };\n\n    setup() {\n        useExternalListener(document.body, \"click\", this.closeRainbowMan);\n        this.state = useState({ isFading: false });\n        this.delay = RainbowMan.rainbowFadeouts[this.props.fadeout];\n        if (this.delay) {\n            useEffect(\n                () => {\n                    const timeout = browser.setTimeout(() => {\n                        this.state.isFading = true;\n                    }, this.delay);\n                    return () => browser.clearTimeout(timeout);\n                },\n                () => []\n            );\n        }\n    }\n\n    onAnimationEnd(ev) {\n        if (this.delay && ev.animationName === \"reward-fading-reverse\") {\n            ev.stopPropagation();\n            this.closeRainbowMan();\n        }\n    }\n\n    closeRainbowMan() {\n        this.props.close();\n    }\n}\n", "import { markEventHandled } from \"@web/core/utils/misc\";\n\nimport {\n    Component,\n    onMounted,\n    onPatched,\n    onWillDestroy,\n    onWillPatch,\n    onWillStart,\n    onWillUnmount,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { loadBundle } from \"@web/core/assets\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\n\n/**\n *\n * @param {import(\"@web/core/utils/hooks\").Ref} [ref]\n * @param {Object} props\n * @param {import(\"@web/core/popover/popover_service\").PopoverServiceAddOptions} [options]\n * @param {function} [props.onSelect]\n * @param {function} [props.onClose]\n */\nexport function useEmojiPicker(ref, props, options = {}) {\n    const targets = [];\n    const state = useState({ isOpen: false });\n    const newOptions = {\n        ...options,\n        onClose: () => {\n            state.isOpen = false;\n            options.onClose?.();\n        },\n    };\n    const popover = usePopover(EmojiPicker, { ...newOptions, animation: false });\n    props.storeScroll = {\n        scrollValue: 0,\n        set: (value) => {\n            props.storeScroll.scrollValue = value;\n        },\n        get: () => {\n            return props.storeScroll.scrollValue;\n        },\n    };\n\n    /**\n     * @param {import(\"@web/core/utils/hooks\").Ref} ref\n     */\n    function add(ref, onSelect, { show = false } = {}) {\n        const toggler = () => toggle(ref, onSelect);\n        targets.push([ref, toggler]);\n        if (!ref.el) {\n            return;\n        }\n        ref.el.addEventListener(\"click\", toggler);\n        ref.el.addEventListener(\"mouseenter\", loadEmoji);\n        if (show) {\n            ref.el.click();\n        }\n    }\n\n    function toggle(ref, onSelect = props.onSelect) {\n        if (popover.isOpen) {\n            popover.close();\n        } else {\n            state.isOpen = true;\n            popover.open(ref.el, { ...props, onSelect });\n        }\n    }\n\n    if (ref) {\n        add(ref);\n    }\n    onMounted(() => {\n        for (const [ref, toggle] of targets) {\n            if (!ref.el) {\n                continue;\n            }\n            ref.el.addEventListener(\"click\", toggle);\n            ref.el.addEventListener(\"mouseenter\", loadEmoji);\n        }\n    });\n    onWillPatch(() => {\n        for (const [ref, toggle] of targets) {\n            if (!ref.el) {\n                continue;\n            }\n            ref.el.removeEventListener(\"click\", toggle);\n            ref.el.removeEventListener(\"mouseenter\", loadEmoji);\n        }\n    });\n    onPatched(() => {\n        for (const [ref, toggle] of targets) {\n            if (!ref.el) {\n                continue;\n            }\n            ref.el.addEventListener(\"click\", toggle);\n            ref.el.addEventListener(\"mouseenter\", loadEmoji);\n        }\n    });\n    Object.assign(state, { add });\n    return state;\n}\n\nconst loadingListeners = [];\n\nexport const loader = {\n    loadEmoji: () => loadBundle(\"web.assets_emoji\"),\n    /** @type {{ emojiValueToShortcode: Object<string, string> }} */\n    loaded: undefined,\n    onEmojiLoaded(cb) {\n        loadingListeners.push(cb);\n    },\n};\n\n/**\n * @returns {import(\"@web/core/emoji_picker/emoji_data\")}\n */\nexport async function loadEmoji() {\n    const res = { categories: [], emojis: [] };\n    try {\n        await loader.loadEmoji();\n        const { getCategories, getEmojis } = odoo.loader.modules.get(\n            \"@web/core/emoji_picker/emoji_data\"\n        );\n        res.categories = getCategories();\n        res.emojis = getEmojis();\n        return res;\n    } catch {\n        // Could be intentional (tour ended successfully while emoji still loading)\n        return res;\n    } finally {\n        if (!loader.loaded) {\n            loader.loaded = { emojiValueToShortcode: {} };\n            for (const emoji of res.emojis) {\n                const value = emoji.codepoints;\n                const shortcode = emoji.shortcodes[0];\n                loader.loaded.emojiValueToShortcode[value] = shortcode;\n                for (const listener of loadingListeners) {\n                    listener();\n                }\n                loadingListeners.length = 0;\n            }\n        }\n    }\n}\n\nexport const EMOJI_PER_ROW = 9;\nexport const EMOJI_PICKER_PROPS = [\"close?\", \"onClose?\", \"onSelect\", \"state?\", \"storeScroll?\"];\n\nexport class EmojiPicker extends Component {\n    static props = EMOJI_PICKER_PROPS;\n    static template = \"web.EmojiPicker\";\n\n    categories = null;\n    emojis = null;\n    shouldScrollElem = null;\n    lastSearchTerm;\n\n    setup() {\n        this.gridRef = useRef(\"emoji-grid\");\n        this.ui = useState(useService(\"ui\"));\n        this.isMobileOS = isMobileOS();\n        this.state = useState({\n            activeEmojiIndex: 0,\n            categoryId: null,\n            recent: JSON.parse(browser.localStorage.getItem(\"web.emoji.frequent\") || \"{}\"),\n            searchTerm: \"\",\n        });\n        const onStorage = (ev) => {\n            if (ev.key === \"web.emoji.frequent\") {\n                this.state.recent = ev.newValue ? JSON.parse(ev.newValue) : {};\n            } else if (ev.key === null) {\n                this.state.recent = {};\n            }\n        };\n        browser.addEventListener(\"storage\", onStorage);\n        onWillDestroy(() => {\n            browser.removeEventListener(\"storage\", onStorage);\n        });\n        useAutofocus();\n        onWillStart(async () => {\n            const { categories, emojis } = await loadEmoji();\n            this.categories = categories;\n            this.emojis = emojis;\n            this.emojiByCodepoints = Object.fromEntries(\n                this.emojis.map((emoji) => [emoji.codepoints, emoji])\n            );\n            this.state.categoryId = this.categories[0]?.sortId;\n            this.recentCategory = {\n                name: \"Frequently used\",\n                displayName: _t(\"Frequently used\"),\n                title: \"\ud83d\udd53\",\n                sortId: 0,\n            };\n        });\n        onMounted(() => {\n            if (this.emojis.length === 0) {\n                return;\n            }\n            this.highlightActiveCategory();\n            if (this.props.storeScroll) {\n                this.gridRef.el.scrollTop = this.props.storeScroll.get();\n            }\n        });\n        onPatched(() => {\n            if (this.emojis.length === 0) {\n                return;\n            }\n            if (this.shouldScrollElem) {\n                this.shouldScrollElem = false;\n                const getElement = () =>\n                    this.gridRef.el.querySelector(\n                        `.o-EmojiPicker-category[data-category=\"${this.state.categoryId}\"`\n                    );\n                const elem = getElement();\n                if (elem) {\n                    elem.scrollIntoView();\n                } else {\n                    this.shouldScrollElem = getElement;\n                }\n            }\n        });\n        useEffect(\n            () => {\n                if (this.searchTerm) {\n                    this.gridRef.el.scrollTop = 0;\n                    this.state.categoryId = null;\n                } else {\n                    if (this.lastSearchTerm) {\n                        this.gridRef.el.scrollTop = 0;\n                    }\n                    this.highlightActiveCategory();\n                }\n                this.lastSearchTerm = this.searchTerm;\n            },\n            () => [this.searchTerm]\n        );\n        onWillUnmount(() => {\n            if (!this.gridRef.el) {\n                return;\n            }\n            if (this.props.storeScroll) {\n                this.props.storeScroll.set(this.gridRef.el.scrollTop);\n            }\n        });\n    }\n\n    get searchTerm() {\n        return this.props.state ? this.props.state.searchTerm : this.state.searchTerm;\n    }\n\n    set searchTerm(value) {\n        if (this.props.state) {\n            this.props.state.searchTerm = value;\n        } else {\n            this.state.searchTerm = value;\n        }\n    }\n\n    get itemsNumber() {\n        return this.recentEmojis.length + this.getEmojis().length;\n    }\n\n    get recentEmojis() {\n        const recent = Object.entries(this.state.recent)\n            .sort(([, usage_1], [, usage_2]) => usage_2 - usage_1)\n            .map(([codepoints]) => this.emojiByCodepoints[codepoints]);\n        if (this.searchTerm && recent.length > 0) {\n            return fuzzyLookup(this.searchTerm, recent, (emoji) =>\n                [emoji.name, ...emoji.keywords, ...emoji.emoticons, ...emoji.shortcodes].join(\" \")\n            );\n        }\n        return recent.slice(0, 42);\n    }\n\n    onClick(ev) {\n        markEventHandled(ev, \"emoji.selectEmoji\");\n    }\n\n    onKeydown(ev) {\n        switch (ev.key) {\n            case \"ArrowUp\": {\n                const newIndex = this.state.activeEmojiIndex - EMOJI_PER_ROW;\n                if (newIndex >= 0) {\n                    this.state.activeEmojiIndex = newIndex;\n                }\n                break;\n            }\n            case \"ArrowDown\": {\n                const newIndex = this.state.activeEmojiIndex + EMOJI_PER_ROW;\n                if (newIndex < this.itemsNumber) {\n                    this.state.activeEmojiIndex = newIndex;\n                }\n                break;\n            }\n            case \"ArrowRight\": {\n                if (this.state.activeEmojiIndex + 1 === this.itemsNumber) {\n                    break;\n                }\n                this.state.activeEmojiIndex++;\n                ev.preventDefault();\n                break;\n            }\n            case \"ArrowLeft\": {\n                const newIndex = Math.max(this.state.activeEmojiIndex - 1, 0);\n                if (newIndex !== this.state.activeEmojiIndex) {\n                    this.state.activeEmojiIndex = newIndex;\n                    ev.preventDefault();\n                }\n                break;\n            }\n            case \"Enter\":\n                ev.preventDefault();\n                this.gridRef.el\n                    .querySelector(\n                        `.o-EmojiPicker-content .o-Emoji[data-index=\"${this.state.activeEmojiIndex}\"]`\n                    )\n                    ?.click();\n                break;\n            case \"Escape\":\n                this.props.close?.();\n                this.props.onClose?.();\n                ev.stopPropagation();\n        }\n    }\n\n    getEmojis() {\n        let emojisToDisplay = [...this.emojis];\n        const recentEmojis = this.recentEmojis;\n        if (recentEmojis.length > 0 && this.searchTerm) {\n            emojisToDisplay = emojisToDisplay.filter((emoji) => !recentEmojis.includes(emoji));\n        }\n        if (this.searchTerm.length > 1) {\n            return fuzzyLookup(this.searchTerm, emojisToDisplay, (emoji) =>\n                [emoji.name, ...emoji.keywords, ...emoji.emoticons, ...emoji.shortcodes].join(\" \")\n            );\n        }\n        return emojisToDisplay;\n    }\n\n    selectCategory(ev) {\n        const id = Number(ev.currentTarget.dataset.id);\n        this.searchTerm = \"\";\n        this.state.categoryId = id;\n        this.shouldScrollElem = true;\n    }\n\n    selectEmoji(ev) {\n        const codepoints = ev.currentTarget.dataset.codepoints;\n        const resetOnSelect = !ev.shiftKey && !this.ui.isSmall;\n        this.props.onSelect(codepoints, resetOnSelect);\n        this.state.recent[codepoints] ??= 0;\n        this.state.recent[codepoints]++;\n        browser.localStorage.setItem(\"web.emoji.frequent\", JSON.stringify(this.state.recent));\n        if (resetOnSelect) {\n            this.gridRef.el.scrollTop = 0;\n            this.props.close?.();\n            this.props.onClose?.();\n        }\n    }\n\n    highlightActiveCategory() {\n        if (!this.gridRef || !this.gridRef.el) {\n            return;\n        }\n        const coords = this.gridRef.el.getBoundingClientRect();\n        const res = document.elementFromPoint(coords.x, coords.y + 1); // +1 for Firefox\n        if (!res) {\n            return;\n        }\n        this.state.categoryId = parseInt(res.dataset.category);\n    }\n}\n", "import { loadBundle } from \"./assets\";\n\nexport async function ensureJQuery() {\n    if (!window.jQuery) {\n        await loadBundle(\"web._assets_jquery\");\n        // allow to instantiate Bootstrap classes via jQuery: e.g. $(...).dropdown\n        const BTS_CLASSES = [\"Carousel\", \"Dropdown\", \"Modal\", \"Popover\", \"Tooltip\", \"Collapse\"];\n        const $ = window.jQuery;\n        for (const CLS of BTS_CLASSES) {\n            const plugin = window[CLS];\n            if (plugin) {\n                const name = plugin.NAME;\n                const JQUERY_NO_CONFLICT = $.fn[name];\n                $.fn[name] = plugin.jQueryInterface;\n                $.fn[name].Constructor = plugin;\n                $.fn[name].noConflict = () => {\n                    $.fn[name] = JQUERY_NO_CONFLICT;\n                    return plugin.jQueryInterface;\n                };\n            }\n        }\n    }\n}\n", "import { browser } from \"../browser/browser\";\nimport { Dialog } from \"../dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"../registry\";\nimport { Tooltip } from \"@web/core/tooltip/tooltip\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { capitalize } from \"../utils/strings\";\n\nimport { Component, useRef, useState, markup } from \"@odoo/owl\";\n\nconst { DateTime } = luxon;\n\n// This props are added by the error handler\nexport const standardErrorDialogProps = {\n    traceback: { type: [String, { value: null }], optional: true },\n    message: { type: String, optional: true },\n    name: { type: String, optional: true },\n    exceptionName: { type: [String, { value: null }], optional: true },\n    data: { type: [Object, { value: null }], optional: true },\n    subType: { type: [String, { value: null }], optional: true },\n    code: { type: [Number, String, { value: null }], optional: true },\n    type: { type: [String, { value: null }], optional: true },\n    serverHost: { type: [String, { value: null }], optional: true },\n    id: { type: [Number, { value: null }], optional: true },\n    model: { type: [String, { value: null }], optional: true },\n    close: Function, // prop added by the Dialog service\n};\n\nexport const odooExceptionTitleMap = new Map(\n    Object.entries({\n        \"odoo.addons.base.models.ir_mail_server.MailDeliveryException\": _t(\"MailDeliveryException\"),\n        \"odoo.exceptions.AccessDenied\": _t(\"Access Denied\"),\n        \"odoo.exceptions.MissingError\": _t(\"Missing Record\"),\n        \"odoo.addons.web.controllers.action.MissingActionError\": _t(\"Missing Action\"),\n        \"odoo.exceptions.UserError\": _t(\"Invalid Operation\"),\n        \"odoo.exceptions.ValidationError\": _t(\"Validation Error\"),\n        \"odoo.exceptions.AccessError\": _t(\"Access Error\"),\n        \"odoo.exceptions.Warning\": _t(\"Warning\"),\n    })\n);\n\n// -----------------------------------------------------------------------------\n// Generic Error Dialog\n// -----------------------------------------------------------------------------\nexport class ErrorDialog extends Component {\n    static template = \"web.ErrorDialog\";\n    static components = { Dialog };\n    static title = _t(\"Odoo Error\");\n    static showTracebackButtonText = _t(\"See technical details\");\n    static hideTracebackButtonText = _t(\"Hide technical details\");\n    static props = { ...standardErrorDialogProps };\n\n    setup() {\n        this.state = useState({\n            showTraceback: false,\n        });\n        this.copyButtonRef = useRef(\"copyButton\");\n        this.popover = usePopover(Tooltip);\n        this.contextDetails = \"Occured \";\n        if (this.props.serverHost) {\n            this.contextDetails += `on ${this.props.serverHost} `;\n        }\n        if (this.props.model && this.props.id) {\n            this.contextDetails += `on model ${this.props.model} and id ${this.props.id} `;\n        }\n        this.contextDetails += `on ${DateTime.now()\n            .setZone(\"UTC\")\n            .toFormat(\"yyyy-MM-dd HH:mm:ss\")} GMT`;\n    }\n\n    showTooltip() {\n        this.popover.open(this.copyButtonRef.el, { tooltip: _t(\"Copied\") });\n        browser.setTimeout(this.popover.close, 800);\n    }\n\n    onClickClipboard() {\n        browser.navigator.clipboard.writeText(\n            `${this.props.name}\\n\\n${this.props.message}\\n\\n${this.contextDetails}\\n\\n${this.props.traceback}`\n        );\n        this.showTooltip();\n    }\n}\n\n// -----------------------------------------------------------------------------\n// Client Error Dialog\n// -----------------------------------------------------------------------------\nexport class ClientErrorDialog extends ErrorDialog {}\nClientErrorDialog.title = _t(\"Odoo Client Error\");\n\n// -----------------------------------------------------------------------------\n// Network Error Dialog\n// -----------------------------------------------------------------------------\nexport class NetworkErrorDialog extends ErrorDialog {}\nNetworkErrorDialog.title = _t(\"Odoo Network Error\");\n\n// -----------------------------------------------------------------------------\n// RPC Error Dialog\n// -----------------------------------------------------------------------------\nexport class RPCErrorDialog extends ErrorDialog {\n    setup() {\n        super.setup();\n        this.inferTitle();\n        this.traceback = this.props.traceback;\n        if (this.props.data && this.props.data.debug) {\n            this.traceback = `${this.props.data.debug}\\nThe above server error caused the following client error:\\n${this.traceback}`;\n        }\n    }\n    inferTitle() {\n        // If the server provides an exception name that we have in a registry.\n        if (this.props.exceptionName && odooExceptionTitleMap.has(this.props.exceptionName)) {\n            this.title = odooExceptionTitleMap.get(this.props.exceptionName).toString();\n            return;\n        }\n        // Fall back to a name based on the error type.\n        if (!this.props.type) {\n            return;\n        }\n        switch (this.props.type) {\n            case \"server\":\n                this.title = _t(\"Odoo Server Error\");\n                break;\n            case \"script\":\n                this.title = _t(\"Odoo Client Error\");\n                break;\n            case \"network\":\n                this.title = _t(\"Odoo Network Error\");\n                break;\n        }\n    }\n\n    onClickClipboard() {\n        browser.navigator.clipboard.writeText(\n            `${this.props.name}\\n\\n${this.props.message}\\n\\n${this.contextDetails}\\n\\n${this.traceback}`\n        );\n        this.showTooltip();\n    }\n}\n\n// -----------------------------------------------------------------------------\n// Warning Dialog\n// -----------------------------------------------------------------------------\nexport class WarningDialog extends Component {\n    static template = \"web.WarningDialog\";\n    static components = { Dialog };\n    static props = {\n        ...standardErrorDialogProps,\n        title: { type: String, optional: true },\n    };\n\n    setup() {\n        this.title = this.inferTitle();\n        const { data, message } = this.props;\n        if (data && data.arguments && data.arguments.length > 0) {\n            this.message = data.arguments[0];\n        } else {\n            this.message = message;\n        }\n    }\n    inferTitle() {\n        if (this.props.exceptionName && odooExceptionTitleMap.has(this.props.exceptionName)) {\n            return odooExceptionTitleMap.get(this.props.exceptionName).toString();\n        }\n        return this.props.title || _t(\"Odoo Warning\");\n    }\n}\n\n// -----------------------------------------------------------------------------\n// Redirect Warning Dialog\n// -----------------------------------------------------------------------------\nexport class RedirectWarningDialog extends Component {\n    static template = \"web.RedirectWarningDialog\";\n    static components = { Dialog };\n    static props = { ...standardErrorDialogProps };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        const { data, subType } = this.props;\n        const [message, actionId, buttonText, additionalContext] = data.arguments;\n        this.title = capitalize(subType) || _t(\"Odoo Warning\");\n        this.message = message;\n        this.actionId = actionId;\n        this.buttonText = buttonText;\n        this.additionalContext = additionalContext;\n    }\n    async onClick() {\n        const options = {};\n        if (this.additionalContext) {\n            options.additionalContext = this.additionalContext;\n        }\n        if (this.actionId.help) {\n            this.actionId.help = markup(this.actionId.help);\n        }\n        await this.actionService.doAction(this.actionId, options);\n        this.props.close();\n    }\n}\n\n// -----------------------------------------------------------------------------\n// Error 504 Dialog\n// -----------------------------------------------------------------------------\nexport class Error504Dialog extends Component {\n    static template = \"web.Error504Dialog\";\n    static components = { Dialog };\n    static props = { ...standardErrorDialogProps };\n}\n\n// -----------------------------------------------------------------------------\n// Expired Session Error Dialog\n// -----------------------------------------------------------------------------\nexport class SessionExpiredDialog extends Component {\n    static template = \"web.SessionExpiredDialog\";\n    static components = { Dialog };\n    static props = { ...standardErrorDialogProps };\n\n    onClick() {\n        browser.location.reload();\n    }\n}\n\nregistry\n    .category(\"error_dialogs\")\n    .add(\"odoo.exceptions.AccessDenied\", WarningDialog)\n    .add(\"odoo.exceptions.AccessError\", WarningDialog)\n    .add(\"odoo.exceptions.MissingError\", WarningDialog)\n    .add(\"odoo.addons.web.controllers.action.MissingActionError\", WarningDialog)\n    .add(\"odoo.exceptions.UserError\", WarningDialog)\n    .add(\"odoo.exceptions.ValidationError\", WarningDialog)\n    .add(\"odoo.exceptions.RedirectWarning\", RedirectWarningDialog)\n    .add(\"odoo.http.SessionExpiredException\", SessionExpiredDialog)\n    .add(\"werkzeug.exceptions.Forbidden\", SessionExpiredDialog)\n    .add(\"504\", Error504Dialog);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"../browser/browser\";\nimport { ConnectionLostError, RPCError, rpc } from \"../network/rpc\";\nimport { registry } from \"../registry\";\nimport {\n    ClientErrorDialog,\n    ErrorDialog,\n    NetworkErrorDialog,\n    RPCErrorDialog,\n} from \"./error_dialogs\";\nimport { UncaughtClientError, ThirdPartyScriptError, UncaughtPromiseError } from \"./error_service\";\n\n/**\n * @typedef {import(\"../../env\").OdooEnv} OdooEnv\n * @typedef {import(\"./error_service\").UncaughtError} UncaughError\n */\n\nconst errorHandlerRegistry = registry.category(\"error_handlers\");\nconst errorDialogRegistry = registry.category(\"error_dialogs\");\nconst errorNotificationRegistry = registry.category(\"error_notifications\");\n\n// -----------------------------------------------------------------------------\n// RPC errors\n// -----------------------------------------------------------------------------\n\n/**\n * @param {OdooEnv} env\n * @param {UncaughError} error\n * @param {Error} originalError\n * @returns {boolean}\n */\nexport function rpcErrorHandler(env, error, originalError) {\n    if (!(error instanceof UncaughtPromiseError)) {\n        return false;\n    }\n    if (originalError instanceof RPCError) {\n        // When an error comes from the server, it can have an exeption name.\n        // (or any string truly). It is used as key in the error dialog from\n        // server registry to know which dialog component to use.\n        // It's how a backend dev can easily map its error to another component.\n        // Note that for a client side exception, we don't use this registry\n        // as we can directly assign a value to `component`.\n        // error is here a RPCError\n        error.unhandledRejectionEvent.preventDefault();\n        const exceptionName = originalError.exceptionName;\n        let ErrorComponent = originalError.Component;\n        if (!ErrorComponent && exceptionName) {\n            if (errorNotificationRegistry.contains(exceptionName)) {\n                const notif = errorNotificationRegistry.get(exceptionName);\n                env.services.notification.add(notif.message || originalError.data.message, notif);\n                return true;\n            }\n            if (errorDialogRegistry.contains(exceptionName)) {\n                ErrorComponent = errorDialogRegistry.get(exceptionName);\n            }\n        }\n        if (!ErrorComponent && originalError.data.context) {\n            const exceptionClass = originalError.data.context.exception_class;\n            if (errorDialogRegistry.contains(exceptionClass)) {\n                ErrorComponent = errorDialogRegistry.get(exceptionClass);\n            }\n        }\n\n        env.services.dialog.add(ErrorComponent || RPCErrorDialog, {\n            traceback: error.traceback,\n            message: originalError.message,\n            name: originalError.name,\n            exceptionName: originalError.exceptionName,\n            data: originalError.data,\n            subType: originalError.subType,\n            code: originalError.code,\n            type: originalError.type,\n            serverHost: error.event?.target?.location.host,\n            id: originalError.id,\n            model: originalError.model,\n        });\n        return true;\n    }\n}\n\nerrorHandlerRegistry.add(\"rpcErrorHandler\", rpcErrorHandler, { sequence: 97 });\n\n// -----------------------------------------------------------------------------\n// Lost connection errors\n// -----------------------------------------------------------------------------\n\nlet connectionLostNotifRemove = null;\n/**\n * @param {OdooEnv} env\n * @param {UncaughError} error\n * @param {Error} originalError\n * @returns {boolean}\n */\nexport function lostConnectionHandler(env, error, originalError) {\n    if (!(error instanceof UncaughtPromiseError)) {\n        return false;\n    }\n    if (originalError instanceof ConnectionLostError) {\n        if (connectionLostNotifRemove) {\n            // notification already displayed (can occur if there were several\n            // concurrent rpcs when the connection was lost)\n            return true;\n        }\n        connectionLostNotifRemove = env.services.notification.add(\n            _t(\"Connection lost. Trying to reconnect...\"),\n            { sticky: true }\n        );\n        let delay = 2000;\n        browser.setTimeout(function checkConnection() {\n            rpc(\"/web/webclient/version_info\", {})\n                .then(function () {\n                    if (connectionLostNotifRemove) {\n                        connectionLostNotifRemove();\n                        connectionLostNotifRemove = null;\n                    }\n                    env.services.notification.add(_t(\"Connection restored. You are back online.\"), {\n                        type: \"info\",\n                    });\n                })\n                .catch(() => {\n                    // exponential backoff, with some jitter\n                    delay = delay * 1.5 + 500 * Math.random();\n                    browser.setTimeout(checkConnection, delay);\n                });\n        }, delay);\n        return true;\n    }\n}\nerrorHandlerRegistry.add(\"lostConnectionHandler\", lostConnectionHandler, { sequence: 98 });\n\n// -----------------------------------------------------------------------------\n// Default handler\n// -----------------------------------------------------------------------------\n\nconst defaultDialogs = new Map([\n    [UncaughtClientError, ClientErrorDialog],\n    [UncaughtPromiseError, ClientErrorDialog],\n    [ThirdPartyScriptError, NetworkErrorDialog],\n]);\n\n/**\n * Handles the errors based on the very general error categories emitted by the\n * error service. Notice how we do not look at the original error at all.\n *\n * @param {OdooEnv} env\n * @param {UncaughError} error\n * @returns {boolean}\n */\nexport function defaultHandler(env, error) {\n    const DialogComponent = defaultDialogs.get(error.constructor) || ErrorDialog;\n    env.services.dialog.add(DialogComponent, {\n        traceback: error.traceback,\n        message: error.message,\n        name: error.name,\n        serverHost: error.event?.target?.location.host,\n    });\n    return true;\n}\nerrorHandlerRegistry.add(\"defaultHandler\", defaultHandler, { sequence: 100 });\n", "import { browser } from \"../browser/browser\";\nimport { registry } from \"../registry\";\nimport { completeUncaughtError, getErrorTechnicalName } from \"./error_utils\";\nimport { isBrowserFirefox, isBrowserChrome } from \"@web/core/browser/feature_detection\";\n\n/**\n * Uncaught Errors have 4 properties:\n * - name: technical name of the error (UncaughtError, ...)\n * - message: short user visible description of the issue (\"Uncaught Cors Error\")\n * - traceback: long description, possibly technical of the issue (such as a traceback)\n * - originalError: the error that was actually being caught. Note that it is not\n *      necessarily an error (for ex, if some code does throw \"boom\")\n */\nexport class UncaughtError extends Error {\n    constructor(message) {\n        super(message);\n        this.name = getErrorTechnicalName(this);\n        this.traceback = null;\n    }\n}\n\nexport class UncaughtClientError extends UncaughtError {\n    constructor(message = \"Uncaught Javascript Error\") {\n        super(message);\n    }\n}\n\nexport class UncaughtPromiseError extends UncaughtError {\n    constructor(message = \"Uncaught Promise\") {\n        super(message);\n        this.unhandledRejectionEvent = null;\n    }\n}\n\nexport class ThirdPartyScriptError extends UncaughtError {\n    constructor(message = \"Third-Party Script Error\") {\n        super(message);\n    }\n}\n\nexport const errorService = {\n    start(env) {\n        function handleError(uncaughtError, retry = true) {\n            let originalError = uncaughtError;\n            while (originalError instanceof Error && \"cause\" in originalError) {\n                originalError = originalError.cause;\n            }\n            for (const [name, handler] of registry.category(\"error_handlers\").getEntries()) {\n                try {\n                    if (handler(env, uncaughtError, originalError)) {\n                        break;\n                    }\n                } catch (e) {\n                    console.error(\n                        `A crash occured in error handler ${name} while handling ${uncaughtError}:`,\n                        e\n                    );\n                    return;\n                }\n            }\n            if (\n                uncaughtError.event &&\n                !uncaughtError.event.defaultPrevented &&\n                uncaughtError.traceback\n            ) {\n                // Log the full traceback instead of letting the browser log the incomplete one\n                uncaughtError.event.preventDefault();\n                console.error(uncaughtError.traceback);\n            }\n        }\n\n        browser.addEventListener(\"error\", async (ev) => {\n            const { colno, error, filename, lineno, message } = ev;\n            const errorsToIgnore = [\n                // Ignore some unnecessary \"ResizeObserver loop limit exceeded\" error in Firefox.\n                \"ResizeObserver loop completed with undelivered notifications.\",\n                // ignore Chrome video internal error: https://crbug.com/809574\n                \"ResizeObserver loop limit exceeded\",\n            ];\n            if (!error && errorsToIgnore.includes(message)) {\n                return;\n            }\n            const isRedactedError = !filename && !lineno && !colno;\n            const isThirdPartyScriptError =\n                isRedactedError ||\n                // Firefox doesn't hide details of errors occuring in third-party scripts, check origin explicitly\n                (isBrowserFirefox() && new URL(filename).origin !== window.location.origin);\n            // Don't display error dialogs for third party script errors unless we are in debug mode\n            if (isThirdPartyScriptError && !odoo.debug) {\n                return;\n            }\n            let uncaughtError;\n            if (isRedactedError) {\n                uncaughtError = new ThirdPartyScriptError();\n                uncaughtError.traceback =\n                    `An error whose details cannot be accessed by the Odoo framework has occurred.\\n` +\n                    `The error probably originates from a JavaScript file served from a different origin.\\n` +\n                    `The full error is available in the browser console.`;\n            } else {\n                uncaughtError = new UncaughtClientError();\n                uncaughtError.event = ev;\n                if (error instanceof Error) {\n                    error.errorEvent = ev;\n                    const annotated = env.debug && env.debug.includes(\"assets\");\n                    await completeUncaughtError(uncaughtError, error, annotated);\n                }\n            }\n            uncaughtError.cause = error;\n            handleError(uncaughtError);\n        });\n\n        browser.addEventListener(\"unhandledrejection\", async (ev) => {\n            const error = ev.reason;\n            let traceback;\n            if (isBrowserChrome() && ev instanceof CustomEvent && error === undefined) {\n                // This fix is ad-hoc to a bug in the Honey Paypal extension\n                // They throw a CustomEvent instead of the specified PromiseRejectionEvent\n                // https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event\n                // Moreover Chrome doesn't seem to sandbox enough the extension, as it seems irrelevant\n                // to have extension's errors in the main business page.\n                // We want to ignore those errors as they are not produced by us, and are parasiting\n                // the navigation. We do this according to the heuristic expressed in the if.\n                if (!odoo.debug) {\n                    return;\n                }\n                traceback =\n                    `Uncaught unknown Error\\n` +\n                    `An unknown error occured. This may be due to a Chrome extension meddling with Odoo.\\n` +\n                    `(Opening your browser console might give you a hint on the error.)`;\n            }\n            const uncaughtError = new UncaughtPromiseError();\n            uncaughtError.unhandledRejectionEvent = ev;\n            uncaughtError.event = ev;\n            uncaughtError.traceback = traceback;\n            if (error instanceof Error) {\n                error.errorEvent = ev;\n                const annotated = env.debug && env.debug.includes(\"assets\");\n                await completeUncaughtError(uncaughtError, error, annotated);\n            }\n            uncaughtError.cause = error;\n            handleError(uncaughtError);\n        });\n    },\n};\n\nregistry.category(\"services\").add(\"error\", errorService, { sequence: 1 });\n", "import { loadJS } from \"../assets\"; // use the real, non patched (in tests), loadJS\n\n/** @typedef {import(\"./error_service\").UncaughtError} UncaughtError */\n\n/**\n * @param {UncaughtError} uncaughtError\n * @param {Error} originalError\n * @returns {string}\n */\nfunction combineErrorNames(uncaughtError, originalError) {\n    const originalErrorName = getErrorTechnicalName(originalError);\n    const uncaughtErrorName = getErrorTechnicalName(uncaughtError);\n    if (originalErrorName === Error.name) {\n        return uncaughtErrorName;\n    } else {\n        return `${uncaughtErrorName} > ${originalErrorName}`;\n    }\n}\n\n/**\n * Returns the full traceback for an error chain based on error causes\n *\n * @param {Error} error\n * @returns {string}\n */\nexport function fullTraceback(error) {\n    let traceback = formatTraceback(error);\n    let current = error.cause;\n    while (current) {\n        traceback += `\\n\\nCaused by: ${\n            current instanceof Error ? formatTraceback(current) : current\n        }`;\n        current = current.cause;\n    }\n    return traceback;\n}\n\n/**\n * Returns the full annotated traceback for an error chain based on error causes\n *\n * @param {Error} error\n * @returns {Promise<string>}\n */\nexport async function fullAnnotatedTraceback(error) {\n    if (error.annotatedTraceback) {\n        return error.annotatedTraceback;\n    }\n    // If we don't call preventDefault  synchronously while handling the error\n    // event, the error will be logged in the console with an unannotated\n    // traceback. This is a problem because annotating a traceback cannot be\n    // done synchronously. To work around this issue, we always call\n    // preventDefault, which means it is never logged but we rethrow the error\n    // after annotating its traceback, which will cause the error to be handled\n    // again after the traceback has been annotated, and this function will be\n    // called again and return synchronously (see above)\n    if (error.errorEvent) {\n        error.errorEvent.preventDefault();\n    }\n    let traceback;\n    try {\n        traceback = await annotateTraceback(error);\n        let current = error.cause;\n        while (current) {\n            traceback += `\\n\\nCaused by: ${\n                current instanceof Error ? await annotateTraceback(current) : current\n            }`;\n            current = current.cause;\n        }\n    } catch (e) {\n        console.warn(\"Failed to annotate traceback for error:\", error, \"failure reason:\", e);\n        traceback = fullTraceback(error);\n    }\n    error.annotatedTraceback = traceback;\n    if (error.errorEvent) {\n        throw error;\n    }\n    return traceback;\n}\n\n/**\n * @param {UncaughtError} uncaughtError\n * @param {Error} originalError\n * @param {boolean} annotated\n * @returns {Promise<void>}\n */\nexport async function completeUncaughtError(uncaughtError, originalError, annotated = false) {\n    uncaughtError.name = combineErrorNames(uncaughtError, originalError);\n    if (annotated) {\n        uncaughtError.traceback = await fullAnnotatedTraceback(originalError);\n    } else {\n        uncaughtError.traceback = fullTraceback(originalError);\n    }\n    if (originalError.message) {\n        uncaughtError.message = `${uncaughtError.message} > ${originalError.message}`;\n    }\n    uncaughtError.cause = originalError;\n}\n\n/**\n * @param {Error} error\n * @returns {string}\n */\nexport function getErrorTechnicalName(error) {\n    return error.name !== Error.name ? error.name : error.constructor.name;\n}\n\n/**\n * Format the traceback of an error. Basically, we just add the error message\n * in the traceback if necessary (Chrome already does it by default, but not\n * other browser.)\n *\n * @param {Error} error\n * @returns {string}\n */\nexport function formatTraceback(error) {\n    let traceback = error.stack;\n    const errorName = getErrorTechnicalName(error);\n    // ensure the proper error name and error message are present in the traceback, no matter the error.stack brower's formatting.\n    // Stack example:\n    // Error: Mock: Can't write value\n    //     _onOpenFormView@http://localhost:8069/web/content/425-baf33f1/web.assets.js:1064:30\n    //     ...\n    const descriptionLine = `${errorName}: ${error.message}`;\n    if (error.stack.split(\"\\n\")[0].trim() !== descriptionLine) {\n        // avoid having the description line twice if already present\n        traceback = `${descriptionLine}\\n${error.stack}`.replace(/\\n/g, \"\\n    \");\n    }\n    return traceback;\n}\n\n/**\n * Returns an annotated traceback from an error. This is asynchronous because\n * it needs to fetch the sourcemaps for each script involved in the error,\n * then compute the correct file/line numbers and add the information to the\n * correct line.\n *\n * @param {Error} error\n * @returns {Promise<string>}\n */\nexport async function annotateTraceback(error) {\n    const traceback = formatTraceback(error);\n    try {\n        await loadJS(\"/web/static/lib/stacktracejs/stacktrace.js\");\n    } catch {\n        return traceback;\n    }\n    // In Firefox, the error stack generated by anonymous code (example: invalid\n    // code in a template) is not compatible with the stacktrace lib. This code\n    // corrects the stack to make it compatible with the lib stacktrace.\n    if (error.stack) {\n        const regex = / line (\\d*) > (Function):(\\d*)/gm;\n        const subst = `:$1`;\n        error.stack = error.stack.replace(regex, subst);\n    }\n    // eslint-disable-next-line no-undef\n    let frames;\n    try {\n        frames = await StackTrace.fromError(error);\n    } catch (e) {\n        // This can crash if the originalError has no stack/stacktrace property\n        console.warn(\"The following error could not be annotated:\", error, e);\n        return traceback;\n    }\n    const lines = traceback.split(\"\\n\");\n    if (lines[lines.length - 1].trim() === \"\") {\n        // firefox traceback have an empty line at the end\n        lines.splice(-1);\n    }\n\n    let lineIndex = 0;\n    let frameIndex = 0;\n    while (frameIndex < frames.length) {\n        const line = lines[lineIndex];\n        // skip lines that have no location information as they don't correspond to a frame\n        if (!line.match(/:\\d+:\\d+\\)?$/)) {\n            lineIndex++;\n            continue;\n        }\n        const frame = frames[frameIndex];\n        const info = ` (${frame.fileName}:${frame.lineNumber})`;\n        lines[lineIndex] = line + info;\n        lineIndex++;\n        frameIndex++;\n    }\n    return lines.join(\"\\n\");\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { _t, translationIsReady } from \"@web/core/l10n/translation\";\nimport { getOrigin } from \"@web/core/utils/urls\";\n\nconst scssErrorNotificationService = {\n    dependencies: [\"notification\"],\n    start(env, { notification }) {\n        const origin = getOrigin();\n        const assets = [...document.styleSheets].filter((sheet) => {\n            return (\n                sheet.href?.includes(\"/web\") &&\n                sheet.href?.includes(\"/assets/\") &&\n                // CORS security rules don't allow reading content in JS\n                new URL(sheet.href, browser.location.origin).origin === origin\n            );\n        });\n        translationIsReady.then(() => {\n            for (const asset of assets) {\n                let cssRules;\n                try {\n                    // The filter above isn't enough to protect against CORS errors when reading\n                    // the cssRules property. Indeed, it seems that if the protocol is http, reading\n                    // that property can also trigger a CORS error, even if the origin is the same.\n                    // Anyway, we never want this line to crash, so we protect it.\n                    // See opw 3746910.\n                    cssRules = asset.cssRules;\n                } catch {\n                    continue;\n                }\n                const lastRule = cssRules?.[cssRules?.length - 1];\n                if (lastRule?.selectorText === \"css_error_message\") {\n                    const message = _t(\n                        \"The style compilation failed. This is an administrator or developer error that must be fixed for the entire database before continuing working. See browser console or server logs for details.\"\n                    );\n                    notification.add(message, {\n                        title: _t(\"Style error\"),\n                        sticky: true,\n                        type: \"danger\",\n                    });\n                    console.log(\n                        lastRule.style.content\n                            .replaceAll(\"\\\\a\", \"\\n\")\n                            .replaceAll(\"\\\\*\", \"*\")\n                            .replaceAll(`\\\\\"`, `\"`)\n                    );\n                }\n            }\n        });\n    },\n};\nregistry.category(\"services\").add(\"scss_error_display\", scssErrorNotificationService);\n", "import { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { getExpressionDisplayedOperators } from \"@web/core/expression_editor/expression_editor_operator_editor\";\nimport {\n    condition,\n    expressionFromTree,\n    treeFromExpression,\n} from \"@web/core/tree_editor/condition_tree\";\nimport { TreeEditor } from \"@web/core/tree_editor/tree_editor\";\nimport { getOperatorEditorInfo } from \"@web/core/tree_editor/tree_editor_operator_editor\";\nimport { getDefaultValue } from \"@web/core/tree_editor/tree_editor_value_editors\";\nimport { getDefaultPath } from \"@web/core/tree_editor/utils\";\nimport { ModelFieldSelector } from \"@web/core/model_field_selector/model_field_selector\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ExpressionEditor extends Component {\n    static template = \"web.ExpressionEditor\";\n    static components = { TreeEditor };\n    static props = {\n        resModel: String,\n        fields: Object,\n        expression: String,\n        update: Function,\n    };\n\n    setup() {\n        onWillStart(() => this.onPropsUpdated(this.props));\n        onWillUpdateProps((nextProps) => this.onPropsUpdated(nextProps));\n    }\n\n    async onPropsUpdated(props) {\n        this.filteredFields = Object.fromEntries(\n            Object.entries(props.fields).filter(([_, fieldDef]) => fieldDef.type !== \"properties\")\n        );\n        try {\n            this.tree = treeFromExpression(props.expression, {\n                getFieldDef: (name) => this.getFieldDef(name, props),\n                distributeNot: !this.isDebugMode,\n            });\n        } catch {\n            this.tree = null;\n        }\n    }\n\n    getFieldDef(name, props = this.props) {\n        if (typeof name === \"string\") {\n            return props.fields[name] || null;\n        }\n        return null;\n    }\n\n    getDefaultCondition() {\n        const defaultPath = getDefaultPath(this.filteredFields);\n        const fieldDef = this.filteredFields[defaultPath];\n        const operator = getExpressionDisplayedOperators(fieldDef)[0];\n        const value = getDefaultValue(fieldDef, operator);\n        return condition(fieldDef.name, operator, value);\n    }\n\n    getDefaultOperator(fieldDef) {\n        return getExpressionDisplayedOperators(fieldDef)[0];\n    }\n\n    getOperatorEditorInfo(fieldDef) {\n        const operators = getExpressionDisplayedOperators(fieldDef);\n        return getOperatorEditorInfo(operators, fieldDef);\n    }\n\n    getPathEditorInfo(resModel, defaultCondition) {\n        if (resModel !== this.props.resModel) {\n            throw new Error(\n                `Expression editor doesn't support tree as value so resModel has to be props.resModel`\n            );\n        }\n        return {\n            component: ModelFieldSelector,\n            extractProps: ({ value, update }) => ({\n                path: value,\n                update,\n                resModel: this.props.resModel,\n                readonly: false,\n                filter: (fieldDef) => fieldDef.name in this.filteredFields,\n                showDebugInput: false,\n                followRelations: false,\n                isDebugMode: this.isDebugMode,\n            }),\n            isSupported: (value) => [0, 1].includes(value) || value in this.filteredFields,\n            // by construction, all values received by the path editor are O/1 or a field (name) in this.props.fields.\n            // (see _leafFromAST in condition_tree.js)\n            stringify: (value) => this.props.fields[value].string,\n            defaultValue: () => defaultCondition.path,\n            message: _t(\"Field properties not supported\"),\n        };\n    }\n\n    get isDebugMode() {\n        return !!this.env.debug;\n    }\n\n    onExpressionChange(expression) {\n        this.props.update(expression);\n    }\n\n    resetExpression() {\n        this.props.update(\"True\");\n    }\n\n    update(tree) {\n        const expression = expressionFromTree(tree, {\n            getFieldDef: (name) => this.getFieldDef(name),\n        });\n        this.props.update(expression);\n    }\n}\n", "import { getDomainDisplayedOperators } from \"@web/core/domain_selector/domain_selector_operator_editor\";\n\nconst EXPRESSION_VALID_OPERATORS = [\n    \"<\",\n    \"<=\",\n    \">\",\n    \">=\",\n    \"between\",\n    \"within\",\n    \"in\",\n    \"not in\",\n    \"=\",\n    \"!=\",\n    \"set\",\n    \"not_set\",\n    \"starts_with\",\n    \"ends_with\",\n    \"is\",\n    \"is_not\",\n];\n\nexport function getExpressionDisplayedOperators(fieldDef) {\n    const operators = getDomainDisplayedOperators(fieldDef);\n    return operators.filter((operator) => EXPRESSION_VALID_OPERATORS.includes(operator));\n}\n", "import { Component, useRef, useState } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { ExpressionEditor } from \"@web/core/expression_editor/expression_editor\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\n\nexport class ExpressionEditorDialog extends Component {\n    static components = { Dialog, ExpressionEditor };\n    static template = \"web.ExpressionEditorDialog\";\n    static props = {\n        close: Function,\n        resModel: String,\n        fields: Object,\n        expression: String,\n        onConfirm: Function,\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.state = useState({\n            expression: this.props.expression,\n        });\n        this.confirmButtonRef = useRef(\"confirm\");\n    }\n\n    get expressionEditorProps() {\n        return {\n            resModel: this.props.resModel,\n            fields: this.props.fields,\n            expression: this.state.expression,\n            update: (expression) => {\n                this.state.expression = expression;\n            },\n        };\n    }\n\n    makeDefaultRecord() {\n        const record = {};\n        for (const [name, { type }] of Object.entries(this.props.fields)) {\n            switch (type) {\n                case \"integer\":\n                case \"float\":\n                case \"monetary\":\n                    record[name] = name === \"id\" ? false : 0;\n                    break;\n                case \"one2many\":\n                case \"many2many\":\n                    record[name] = [];\n                    break;\n                default:\n                    record[name] = false;\n            }\n        }\n        return record;\n    }\n\n    async onConfirm() {\n        this.confirmButtonRef.el.disabled = true;\n        const record = this.makeDefaultRecord();\n        const evalContext = { ...user.context, ...record };\n        try {\n            evaluateExpr(this.state.expression, evalContext);\n        } catch {\n            if (this.confirmButtonRef.el) {\n                this.confirmButtonRef.el.disabled = false;\n            }\n            this.notification.add(_t(\"Expression is invalid. Please correct it\"), {\n                type: \"danger\",\n            });\n            return;\n        }\n        this.props.onConfirm(this.state.expression);\n        this.props.close();\n    }\n\n    onDiscard() {\n        this.props.close();\n    }\n}\n", "import { Cache } from \"@web/core/utils/cache\";\nimport { Domain } from \"@web/core/domain\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * @typedef {Object} LoadFieldsOptions\n * @property {string[]|false} [fieldNames]\n * @property {string[]} [attributes]\n */\n\nexport const fieldService = {\n    dependencies: [\"orm\"],\n    async: [\"loadFields\", \"loadPath\", \"loadPropertyDefinitions\"],\n    start(env, { orm }) {\n        const cache = new Cache(\n            (resModel, options) => {\n                return orm\n                    .call(resModel, \"fields_get\", [options.fieldNames, options.attributes])\n                    .catch((error) => {\n                        cache.clear(resModel, options);\n                        return Promise.reject(error);\n                    });\n            },\n            (resModel, options) =>\n                JSON.stringify([resModel, options.fieldNames, options.attributes])\n        );\n\n        env.bus.addEventListener(\"CLEAR-CACHES\", () => cache.invalidate());\n\n        /**\n         * @param {string} resModel\n         * @param {LoadFieldsOptions} [options]\n         * @returns {Promise<object>}\n         */\n        async function loadFields(resModel, options = {}) {\n            if (typeof resModel !== \"string\" || !resModel) {\n                throw new Error(`Invalid model name: ${resModel}`);\n            }\n            return cache.read(resModel, options);\n        }\n\n        /**\n         * @param {Object} fieldDefs\n         * @param {string} name\n         * @param {import(\"@web/core/domain\").DomainListRepr} [domain=[]]\n         * @returns {Promise<Object>}\n         */\n        async function _loadPropertyDefinitions(fieldDefs, name, domain = []) {\n            const {\n                definition_record: definitionRecord,\n                definition_record_field: definitionRecordField,\n            } = fieldDefs[name];\n            const definitionRecordModel = fieldDefs[definitionRecord].relation;\n\n            domain = Domain.and([[[definitionRecordField, \"!=\", false]], domain]).toList();\n\n            const result = await orm.webSearchRead(definitionRecordModel, domain, {\n                specification: {\n                    display_name: {},\n                    [definitionRecordField]: {},\n                },\n            });\n\n            const definitions = {};\n            for (const record of result.records) {\n                for (const definition of record[definitionRecordField]) {\n                    definitions[definition.name] = {\n                        is_property: true,\n                        // for now, all properties are searchable but their definitions don't contain that info\n                        searchable: true,\n                        // differentiate definitions with same name but on different parent\n                        record_id: record.id,\n                        record_name: record.display_name,\n                        ...definition,\n                    };\n                }\n            }\n            return definitions;\n        }\n\n        /**\n         * @param {string} resModel\n         * @param {string} fieldName\n         * @param {import(\"@web/core/domain\").DomainListRepr} [domain]\n         * @returns {Promise<object[]>}\n         */\n        async function loadPropertyDefinitions(resModel, fieldName, domain) {\n            const fieldDefs = await loadFields(resModel);\n            return _loadPropertyDefinitions(fieldDefs, fieldName, domain);\n        }\n\n        /**\n         * @param {string|null} resModel valid model name or null (case virtual)\n         * @param {Object|null} fieldDefs\n         * @param {string[]} names\n         */\n        async function _loadPath(resModel, fieldDefs, names) {\n            if (!fieldDefs) {\n                return { isInvalid: \"path\", names, modelsInfo: [] };\n            }\n\n            const [name, ...remainingNames] = names;\n            const modelsInfo = [{ resModel, fieldDefs }];\n            if (resModel === \"*\" && remainingNames.length) {\n                return { isInvalid: \"path\", names, modelsInfo };\n            }\n\n            const fieldDef = fieldDefs[name];\n            if ((name !== \"*\" && !fieldDef) || (name === \"*\" && remainingNames.length)) {\n                return { isInvalid: \"path\", names, modelsInfo };\n            }\n\n            if (!remainingNames.length) {\n                return { names, modelsInfo };\n            }\n\n            let subResult;\n            if (fieldDef.relation) {\n                subResult = await _loadPath(\n                    fieldDef.relation,\n                    await loadFields(fieldDef.relation),\n                    remainingNames\n                );\n            } else if (fieldDef.type === \"properties\") {\n                subResult = await _loadPath(\n                    \"*\",\n                    await _loadPropertyDefinitions(fieldDefs, name),\n                    remainingNames\n                );\n            }\n\n            if (subResult) {\n                const result = {\n                    names,\n                    modelsInfo: [...modelsInfo, ...subResult.modelsInfo],\n                };\n                if (subResult.isInvalid) {\n                    result.isInvalid = \"path\";\n                }\n                return result;\n            }\n\n            return { isInvalid: \"path\", names, modelsInfo };\n        }\n\n        /**\n         * Note: the symbol * can be used at the end of path (e.g path=\"*\" or path=\"user_id.*\").\n         * It says to load the fields of the appropriate model.\n         * @param {string} resModel\n         * @param {string} path\n         * @returns {Promise<Object>}\n         */\n        async function loadPath(resModel, path = \"*\") {\n            const fieldDefs = await loadFields(resModel);\n            if (typeof path !== \"string\" || !path) {\n                throw new Error(`Invalid path: ${path}`);\n            }\n            return _loadPath(resModel, fieldDefs, path.split(\".\"));\n        }\n\n        return { loadFields, loadPath, loadPropertyDefinitions };\n    },\n};\n\nregistry.category(\"services\").add(\"field\", fieldService);\n", "import { Component, onMounted, useRef, useState } from \"@odoo/owl\";\nimport { useFileUploader } from \"@web/core/utils/files\";\n\n/**\n * Custom file input\n *\n * Component representing a customized input of type file. It takes a sub-template\n * in its default t-slot and uses it as the trigger to open the file upload\n * prompt.\n * @extends Component\n *\n * Props:\n * @param {string} [props.acceptedFileExtensions='*'] Comma-separated\n *      list of authorized file extensions (default to all).\n * @param {string} [props.route='/web/binary/upload'] Route called when\n *      a file is uploaded in the input.\n * @param {string} [props.resId]\n * @param {string} [props.resModel]\n * @param {string} [props.multiUpload=false] Whether the input should allow\n *      to upload multiple files at once.\n */\nexport class FileInput extends Component {\n    static template = \"web.FileInput\";\n    static defaultProps = {\n        acceptedFileExtensions: \"*\",\n        hidden: false,\n        multiUpload: false,\n        onUpload: () => {},\n        route: \"/web/binary/upload_attachment\",\n        beforeOpen: async () => true,\n    };\n    static props = {\n        acceptedFileExtensions: { type: String, optional: true },\n        autoOpen: { type: Boolean, optional: true },\n        hidden: { type: Boolean, optional: true },\n        multiUpload: { type: Boolean, optional: true },\n        onUpload: { type: Function, optional: true },\n        beforeOpen: { type: Function, optional: true },\n        resId: { type: Number, optional: true },\n        resModel: { type: String, optional: true },\n        route: { type: String, optional: true },\n        \"*\": true,\n    };\n\n    setup() {\n        this.uploadFiles = useFileUploader();\n        this.fileInputRef = useRef(\"file-input\");\n        this.state = useState({\n            // Disables upload button if currently uploading.\n            isDisable: false,\n        });\n\n        onMounted(() => {\n            if (this.props.autoOpen) {\n                this.onTriggerClicked();\n            }\n        });\n    }\n\n    get httpParams() {\n        const { resId, resModel } = this.props;\n        const params = {\n            csrf_token: odoo.csrf_token,\n            ufile: [...this.fileInputRef.el.files],\n        };\n        if (resModel) {\n            params.model = resModel;\n        }\n        if (resId !== undefined) {\n            params.id = resId;\n        }\n        return params;\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Upload an attachment to the given route with the given parameters:\n     * - ufile: list of files contained in the file input\n     * - csrf_token: CSRF token provided by the odoo global object\n     * - resModel: a specific model which will be given when creating the attachment\n     * - resId: the id of the resModel target instance\n     */\n    async onFileInputChange() {\n        this.state.isDisable = true;\n        const parsedFileData = await this.uploadFiles(this.props.route, this.httpParams);\n        if (parsedFileData) {\n            // When calling onUpload, also pass the files to allow to get data like their names\n            this.props.onUpload(\n                parsedFileData,\n                this.fileInputRef.el ? this.fileInputRef.el.files : []\n            );\n            // Because the input would not trigger this method if the same file name is uploaded,\n            // we must clear the value after handling the upload\n            this.fileInputRef.el.value = null;\n        }\n        this.state.isDisable = false;\n    }\n\n    /**\n     * Redirect clicks from the trigger element to the input.\n     */\n    async onTriggerClicked() {\n        if (await this.props.beforeOpen()) {\n            this.fileInputRef.el.click();\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"../utils/hooks\";\nimport { ConfirmationDialog } from \"../confirmation_dialog/confirmation_dialog\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class FileUploadProgressBar extends Component {\n    static template = \"web.FileUploadProgressBar\";\n    static props = {\n        fileUpload: { type: Object },\n    };\n\n    setup() {\n        this.dialogService = useService(\"dialog\");\n    }\n\n    onCancel() {\n        if (!this.props.fileUpload.xhr) {\n            return;\n        }\n        this.dialogService.add(ConfirmationDialog, {\n            body: _t(\"Do you really want to cancel the upload of %s?\", this.props.fileUpload.title),\n            confirm: () => {\n                this.props.fileUpload.xhr.abort();\n            },\n        });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class FileUploadProgressContainer extends Component {\n    static template = \"web.FileUploadProgressContainer\";\n    static props = {\n        Component: { optional: false },\n        shouldDisplay: { type: Function, optional: true },\n        fileUploads: { type: Object },\n    };\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { FileUploadProgressBar } from \"./file_upload_progress_bar\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class FileUploadProgressRecord extends Component {\n    static template = \"\";\n    static components = {\n        FileUploadProgressBar,\n    };\n    static props = {\n        fileUpload: Object,\n        selector: { type: String, optional: true },\n    };\n    getProgressTexts() {\n        const fileUpload = this.props.fileUpload;\n        const percent = Math.round(fileUpload.progress * 100);\n        if (percent === 100) {\n            return {\n                left: _t(\"Processing...\"),\n                right: \"\",\n            };\n        } else {\n            const mbLoaded = Math.round(fileUpload.loaded / 1000000);\n            const mbTotal = Math.round(fileUpload.total / 1000000);\n            return {\n                left: _t(\"Uploading... (%s%)\", percent),\n                right: _t(\"(%(mbLoaded)s/%(mbTotal)sMB)\", { mbLoaded, mbTotal }),\n            };\n        }\n    }\n}\n\nexport class FileUploadProgressKanbanRecord extends FileUploadProgressRecord {\n    static template = \"web.FileUploadProgressKanbanRecord\";\n}\n\nexport class FileUploadProgressDataRow extends FileUploadProgressRecord {\n    static template = \"web.FileUploadProgressDataRow\";\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"../registry\";\n\nimport { EventBus, reactive } from \"@odoo/owl\";\n\nexport const fileUploadService = {\n    dependencies: [\"notification\"],\n    /**\n     * Overridden during tests to return a mocked XHR.\n     *\n     * @private\n     * @returns {XMLHttpRequest}\n     */\n    createXhr() {\n        return new window.XMLHttpRequest();\n    },\n\n    start(env, { notificationService }) {\n        const uploads = reactive({});\n        let nextId = 1;\n        const bus = new EventBus();\n\n        /**\n         * @param {string}                          route\n         * @param {FileList|Array<File>}            files\n         * @param {Object}                          [params]\n         * @param {function(formData): void}        [params.buildFormData]\n         * @param {Boolean}                         [params.displayErrorNotification]\n         * @returns {reactive}                      upload\n         * @returns {XMLHttpRequest}                upload.xhr\n         * @returns {FormData}                      upload.data\n         * @returns {Number}                        upload.progress\n         * @returns {Number}                        upload.loaded\n         * @returns {Number}                        upload.total\n         * @returns {String}                        upload.title\n         * @returns {String||undefined}             upload.type\n         */\n        const upload = async (route, files, params = {}) => {\n            const xhr = this.createXhr();\n            xhr.open(\"POST\", route);\n            const formData = new FormData();\n            formData.append(\"csrf_token\", odoo.csrf_token);\n            for (const file of files) {\n                formData.append(\"ufile\", file);\n            }\n            if (params.buildFormData) {\n                params.buildFormData(formData);\n            }\n            const upload = reactive({\n                id: nextId++,\n                xhr,\n                data: formData,\n                progress: 0,\n                loaded: 0,\n                total: 0,\n                state: \"pending\",\n                title: files.length === 1 ? files[0].name : _t(\"%s Files\", files.length),\n                type: files.length === 1 ? files[0].type : undefined,\n            });\n            uploads[upload.id] = upload;\n            // Progress listener\n            xhr.upload.addEventListener(\"progress\", async (ev) => {\n                upload.progress = ev.loaded / ev.total;\n                upload.loaded = ev.loaded;\n                upload.total = ev.total;\n                upload.state = \"loading\";\n            });\n            // Load listener\n            xhr.addEventListener(\"load\", () => {\n                delete uploads[upload.id];\n                upload.state = \"loaded\";\n                bus.trigger(\"FILE_UPLOAD_LOADED\", { upload });\n            });\n            // Error listener\n            xhr.addEventListener(\"error\", async () => {\n                delete uploads[upload.id];\n                upload.state = \"error\";\n                // Disable this option if you need more explicit error handling.\n                if (\n                    params.displayErrorNotification !== undefined &&\n                    params.displayErrorNotification\n                ) {\n                    notificationService.add(_t(\"An error occured while uploading.\"), {\n                        title: _t(\"Error\"),\n                        sticky: true,\n                    });\n                }\n                bus.trigger(\"FILE_UPLOAD_ERROR\", { upload });\n            });\n            // Abort listener, considered as error\n            xhr.addEventListener(\"abort\", async () => {\n                delete uploads[upload.id];\n                upload.state = \"abort\";\n                bus.trigger(\"FILE_UPLOAD_ERROR\", { upload });\n            });\n            xhr.send(formData);\n            bus.trigger(\"FILE_UPLOAD_ADDED\", { upload });\n            return upload;\n        };\n\n        return { bus, upload, uploads };\n    },\n};\n\nregistry.category(\"services\").add(\"file_upload\", fileUploadService);\n", "import { url } from \"@web/core/utils/urls\";\n\nexport const FileModelMixin = T => class extends T {\n    access_token;\n    checksum;\n    extension;\n    filename;\n    id;\n    mimetype;\n    name;\n    type;\n    /** @type {string} */\n    tmpUrl;\n    /** @type {string} */\n    url;\n    /** @type {boolean} */\n    uploading;\n\n    get defaultSource() {\n        const route = url(this.urlRoute, this.urlQueryParams);\n        const encodedRoute = encodeURIComponent(route);\n        if (this.isPdf) {\n            return `/web/static/lib/pdfjs/web/viewer.html?file=${encodedRoute}#pagemode=none`;\n        }\n        if (this.isUrlYoutube) {\n            const urlArr = this.url.split(\"/\");\n            let token = urlArr[urlArr.length - 1];\n            if (token.includes(\"watch\")) {\n                token = token.split(\"v=\")[1];\n                const amp = token.indexOf(\"&\");\n                if (amp !== -1) {\n                    token = token.substring(0, amp);\n                }\n            }\n            return `https://www.youtube.com/embed/${token}`;\n        }\n        return route;\n    }\n\n    get displayName() {\n        return this.name || this.filename;\n    }\n\n    get downloadUrl() {\n        return url(this.urlRoute, { ...this.urlQueryParams, download: true });\n    }\n\n    get isImage() {\n        const imageMimetypes = [\n            \"image/bmp\",\n            \"image/gif\",\n            \"image/jpeg\",\n            \"image/png\",\n            \"image/svg+xml\",\n            \"image/tiff\",\n            \"image/x-icon\",\n            \"image/webp\",\n        ];\n        return imageMimetypes.includes(this.mimetype);\n    }\n\n    get isPdf() {\n        return this.mimetype && this.mimetype.startsWith(\"application/pdf\");\n    }\n\n    get isText() {\n        const textMimeType = [\n            \"application/javascript\",\n            \"application/json\",\n            \"text/css\",\n            \"text/html\",\n            \"text/plain\",\n        ];\n        return textMimeType.includes(this.mimetype);\n    }\n\n    get isUrl() {\n        return this.type === \"url\" && this.url;\n    }\n\n    get isUrlYoutube() {\n        return !!this.url && this.url.includes(\"youtu\");\n    }\n\n    get isVideo() {\n        const videoMimeTypes = [\"audio/mpeg\", \"video/x-matroska\", \"video/mp4\", \"video/webm\"];\n        return videoMimeTypes.includes(this.mimetype);\n    }\n\n    get isViewable() {\n        return (\n            (this.isText || this.isImage || this.isVideo || this.isPdf || this.isUrlYoutube) &&\n            !this.uploading\n        );\n    }\n\n    /**\n     * @returns {Object}\n     */\n    get urlQueryParams() {\n        if (this.uploading && this.tmpUrl) {\n            return {};\n        }\n        const params = {\n            access_token: this.access_token,\n            filename: this.name,\n            unique: this.checksum,\n        };\n        for (const prop in params) {\n            if (!params[prop]) {\n                delete params[prop];\n            }\n        }\n        return params;\n    }\n\n    /**\n     * @returns {string}\n     */\n    get urlRoute() {\n        if (this.isUrl) {\n            return this.url;\n        }\n        if (this.uploading && this.tmpUrl) {\n            return this.tmpUrl;\n        }\n        return this.isImage ? `/web/image/${this.id}` : `/web/content/${this.id}`;\n    }\n}\n\nexport class FileModel extends FileModelMixin(Object) {}\n", "import { Component, useRef, useState } from \"@odoo/owl\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} File\n * @property {string} displayName\n * @property {string} downloadUrl\n * @property {boolean} [isImage]\n * @property {boolean} [isPdf]\n * @property {boolean} [isVideo]\n * @property {boolean} [isText]\n * @property {string} [defaultSource]\n * @property {boolean} [isUrlYoutube]\n * @property {string} [mimetype]\n * @property {boolean} [isViewable]\n * @typedef {Object} Props\n * @property {Array<File>} files\n * @property {number} startIndex\n * @property {function} close\n * @property {boolean} [modal]\n * @extends {Component<Props, Env>}\n */\nexport class FileViewer extends Component {\n    static template = \"web.FileViewer\";\n    static components = {};\n    static props = [\"files\", \"startIndex\", \"close?\", \"modal?\"];\n    static defaultProps = {\n        modal: true,\n    };\n\n    setup() {\n        useAutofocus();\n        this.imageRef = useRef(\"image\");\n        this.zoomerRef = useRef(\"zoomer\");\n\n        this.isDragging = false;\n        this.dragStartX = 0;\n        this.dragStartY = 0;\n\n        this.scrollZoomStep = 0.1;\n        this.zoomStep = 0.5;\n        this.minScale = 0.5;\n        this.translate = {\n            dx: 0,\n            dy: 0,\n            x: 0,\n            y: 0,\n        };\n\n        this.state = useState({\n            index: this.props.startIndex,\n            file: this.props.files[this.props.startIndex],\n            imageLoaded: false,\n            scale: 1,\n            angle: 0,\n        });\n        this.ui = useState(useService(\"ui\"));\n    }\n\n    onImageLoaded() {\n        this.state.imageLoaded = true;\n    }\n\n    close() {\n        this.props.close && this.props.close();\n    }\n\n    next() {\n        const last = this.props.files.length - 1;\n        this.activateFile(this.state.index === last ? 0 : this.state.index + 1);\n    }\n\n    previous() {\n        const last = this.props.files.length - 1;\n        this.activateFile(this.state.index === 0 ? last : this.state.index - 1);\n    }\n\n    activateFile(index) {\n        this.state.index = index;\n        this.state.file = this.props.files[index];\n    }\n\n    onKeydown(ev) {\n        switch (ev.key) {\n            case \"ArrowRight\":\n                this.next();\n                break;\n            case \"ArrowLeft\":\n                this.previous();\n                break;\n            case \"Escape\":\n                this.close();\n                break;\n            case \"q\":\n                this.close();\n                break;\n        }\n        if (this.state.file.isImage) {\n            switch (ev.key) {\n                case \"r\":\n                    this.rotate();\n                    break;\n                case \"+\":\n                    this.zoomIn();\n                    break;\n                case \"-\":\n                    this.zoomOut();\n                    break;\n                case \"0\":\n                    this.resetZoom();\n                    break;\n            }\n        }\n    }\n\n    /**\n     * @param {Event} ev\n     */\n    onWheelImage(ev) {\n        if (ev.deltaY > 0) {\n            this.zoomOut({ scroll: true });\n        } else {\n            this.zoomIn({ scroll: true });\n        }\n    }\n\n    /**\n     * @param {DragEvent} ev\n     */\n    onMousedownImage(ev) {\n        if (this.isDragging) {\n            return;\n        }\n        if (ev.button !== 0) {\n            return;\n        }\n        this.isDragging = true;\n        this.dragStartX = ev.clientX;\n        this.dragStartY = ev.clientY;\n    }\n\n    onMouseupImage() {\n        if (!this.isDragging) {\n            return;\n        }\n        this.isDragging = false;\n        this.translate.x += this.translate.dx;\n        this.translate.y += this.translate.dy;\n        this.translate.dx = 0;\n        this.translate.dy = 0;\n        this.updateZoomerStyle();\n    }\n\n    /**\n     * @param {DragEvent}\n     */\n    onMousemoveView(ev) {\n        if (!this.isDragging) {\n            return;\n        }\n        this.translate.dx = ev.clientX - this.dragStartX;\n        this.translate.dy = ev.clientY - this.dragStartY;\n        this.updateZoomerStyle();\n    }\n\n    resetZoom() {\n        this.state.scale = 1;\n        this.updateZoomerStyle();\n    }\n\n    rotate() {\n        this.state.angle += 90;\n    }\n\n    /**\n     * @param {{ scroll?: boolean }}\n     */\n    zoomIn({ scroll = false } = {}) {\n        this.state.scale = this.state.scale + (scroll ? this.scrollZoomStep : this.zoomStep);\n        this.updateZoomerStyle();\n    }\n\n    /**\n     * @param {{ scroll?: boolean }}\n     */\n    zoomOut({ scroll = false } = {}) {\n        if (this.state.scale === this.minScale) {\n            return;\n        }\n        const unflooredAdaptedScale =\n            this.state.scale - (scroll ? this.scrollZoomStep : this.zoomStep);\n        this.state.scale = Math.max(this.minScale, unflooredAdaptedScale);\n        this.updateZoomerStyle();\n    }\n\n    updateZoomerStyle() {\n        const tx =\n            this.imageRef.el.offsetWidth * this.state.scale > this.zoomerRef.el.offsetWidth\n                ? this.translate.x + this.translate.dx\n                : 0;\n        const ty =\n            this.imageRef.el.offsetHeight * this.state.scale > this.zoomerRef.el.offsetHeight\n                ? this.translate.y + this.translate.dy\n                : 0;\n        if (tx === 0) {\n            this.translate.x = 0;\n        }\n        if (ty === 0) {\n            this.translate.y = 0;\n        }\n        this.zoomerRef.el.style = \"transform: \" + `translate(${tx}px, ${ty}px)`;\n    }\n\n    get imageStyle() {\n        let style =\n            \"transform: \" +\n            `scale3d(${this.state.scale}, ${this.state.scale}, 1) ` +\n            `rotate(${this.state.angle}deg);`;\n\n        if (this.state.angle % 180 !== 0) {\n            style += `max-height: ${window.innerWidth}px; max-width: ${window.innerHeight}px;`;\n        } else {\n            style += \"max-height: 100%; max-width: 100%;\";\n        }\n        return style;\n    }\n\n    onClickPrint() {\n        const printWindow = window.open(\"about:blank\", \"_new\");\n        printWindow.document.open();\n        printWindow.document.write(`\n                <html>\n                    <head>\n                        <script>\n                            function onloadImage() {\n                                setTimeout('printImage()', 10);\n                            }\n                            function printImage() {\n                                window.print();\n                                window.close();\n                            }\n                        </script>\n                    </head>\n                    <body onload='onloadImage()'>\n                        <img src=\"${this.state.file.defaultSource}\" alt=\"\"/>\n                    </body>\n                </html>`);\n        printWindow.document.close();\n    }\n}\n", "import { onWillDestroy } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { FileViewer } from \"./file_viewer\";\n\nlet id = 1;\n\nexport function createFileViewer() {\n    const fileViewerId = `web.file_viewer${id++}`;\n    /**\n     * @param {import(\"@web/core/file_viewer/file_viewer\").FileViewer.props.files[]} file\n     * @param {import(\"@web/core/file_viewer/file_viewer\").FileViewer.props.files} files\n     */\n    function open(file, files = [file]) {\n        if (!file.isViewable) {\n            return;\n        }\n        if (files.length > 0) {\n            const viewableFiles = files.filter((file) => file.isViewable);\n            const index = viewableFiles.indexOf(file);\n            registry.category(\"main_components\").add(fileViewerId, {\n                Component: FileViewer,\n                props: { files: viewableFiles, startIndex: index, close },\n            });\n        }\n    }\n\n    function close() {\n        registry.category(\"main_components\").remove(fileViewerId);\n    }\n    return { open, close };\n}\n\nexport function useFileViewer() {\n    const { open, close } = createFileViewer();\n    onWillDestroy(close);\n    return { open, close };\n}\n", "import { useService } from \"@web/core/utils/hooks\";\n\nimport { useEffect } from \"@odoo/owl\";\n\n/**\n * This hook will register/unregister the given registration\n * when the caller component will mount/unmount.\n *\n * @param {string} hotkey\n * @param {import(\"./hotkey_service\").HotkeyCallback} callback\n * @param {import(\"./hotkey_service\").HotkeyOptions} [options] additional options\n */\nexport function useHotkey(hotkey, callback, options = {}) {\n    const hotkeyService = useService(\"hotkey\");\n    useEffect(\n        () => hotkeyService.add(hotkey, callback, options),\n        () => []\n    );\n}\n", "import { isMacOS } from \"../browser/feature_detection\";\nimport { registry } from \"../registry\";\nimport { browser } from \"../browser/browser\";\nimport { getVisibleElements } from \"../utils/ui\";\n\n/**\n * @typedef {(context: { area: HTMLElement, target: EventTarget }) => void} HotkeyCallback\n *\n * @typedef {Object} HotkeyOptions\n * @property {boolean} [allowRepeat]\n *  allow registration to perform multiple times when hotkey is held down\n * @property {boolean} [bypassEditableProtection]\n *  if true the hotkey service will call this registration\n *  even if an editable element is focused\n * @property {boolean} [global]\n *  allow registration to perform no matter the UI active element\n * @property {() => HTMLElement} [area]\n *  adds a restricted operating area for this hotkey\n * @property {() => boolean} [isAvailable]\n *  adds a validation before calling the hotkey registration's callback\n * @property {() => HTMLElement} [withOverlay]\n *  provides the element on which the overlay should be displayed\n *  Please note that if provided the hotkey will only work with\n *  the overlay access key, similarly to all [data-hotkey] DOM attributes.\n *\n * @typedef {HotkeyOptions & {\n *  hotkey: string,\n *  callback: HotkeyCallback,\n *  activeElement: HTMLElement,\n * }} HotkeyRegistration\n */\n\nconst ALPHANUM_KEYS = \"abcdefghijklmnopqrstuvwxyz0123456789\".split(\"\");\nconst NAV_KEYS = [\n    \"arrowleft\",\n    \"arrowright\",\n    \"arrowup\",\n    \"arrowdown\",\n    \"pageup\",\n    \"pagedown\",\n    \"home\",\n    \"end\",\n    \"backspace\",\n    \"enter\",\n    \"tab\",\n    \"delete\",\n    \"space\",\n];\nconst MODIFIERS = [\"alt\", \"control\", \"shift\"];\nconst AUTHORIZED_KEYS = [...ALPHANUM_KEYS, ...NAV_KEYS, \"escape\"];\n\n/**\n * Get the actual hotkey being pressed.\n *\n * @param {KeyboardEvent} ev\n * @returns {string} the active hotkey, in lowercase\n */\nexport function getActiveHotkey(ev) {\n    if (!ev.key) {\n        // Chrome may trigger incomplete keydown events under certain circumstances.\n        // E.g. when using browser built-in autocomplete on an input.\n        // See https://stackoverflow.com/questions/59534586/google-chrome-fires-keydown-event-when-form-autocomplete\n        return \"\";\n    }\n    if (ev.isComposing) {\n        // This case happens with an IME for example: we let it handle all key events.\n        return \"\";\n    }\n    const hotkey = [];\n\n    // ------- Modifiers -------\n    // Modifiers are pushed in ascending order to the hotkey.\n    if (isMacOS() ? ev.ctrlKey : ev.altKey) {\n        hotkey.push(\"alt\");\n    }\n    if (isMacOS() ? ev.metaKey : ev.ctrlKey) {\n        hotkey.push(\"control\");\n    }\n    if (ev.shiftKey) {\n        hotkey.push(\"shift\");\n    }\n\n    // ------- Key -------\n    let key = ev.key.toLowerCase();\n\n    // The browser space is natively \" \", we want \"space\" for esthetic reasons\n    if (key === \" \") {\n        key = \"space\";\n    }\n\n    // Identify if the user has tapped on the number keys above the text keys.\n    if (ev.code && ev.code.indexOf(\"Digit\") === 0) {\n        key = ev.code.slice(-1);\n    }\n    // Prefer physical keys for non-latin keyboard layout.\n    if (!AUTHORIZED_KEYS.includes(key) && ev.code && ev.code.indexOf(\"Key\") === 0) {\n        key = ev.code.slice(-1).toLowerCase();\n    }\n    // Make sure we do not duplicate a modifier key\n    if (!MODIFIERS.includes(key)) {\n        hotkey.push(key);\n    }\n\n    return hotkey.join(\"+\");\n}\n\nexport const hotkeyService = {\n    dependencies: [\"ui\"],\n    // Be aware that all odoo hotkeys are designed with this modifier in mind,\n    // so changing the overlay modifier may conflict with some shortcuts.\n    overlayModifier: \"alt\",\n    start(env, { ui }) {\n        /** @type {Map<number, HotkeyRegistration>} */\n        const registrations = new Map();\n        let nextToken = 0;\n        let overlaysVisible = false;\n\n        addListeners(browser);\n\n        function addListeners(target) {\n            target.addEventListener(\"keydown\", onKeydown);\n            target.addEventListener(\"keyup\", removeHotkeyOverlays);\n            target.addEventListener(\"blur\", removeHotkeyOverlays);\n            target.addEventListener(\"click\", removeHotkeyOverlays);\n        }\n\n        /**\n         * Handler for keydown events.\n         * Verifies if the keyboard event can be dispatched or not.\n         * Rules sequence to forbid dispatching :\n         * - UI is blocked\n         * - the pressed key is not whitelisted\n         *\n         * @param {KeyboardEvent} event\n         */\n        function onKeydown(event) {\n            if (event.code && event.code.indexOf(\"Numpad\") === 0 && /^\\d$/.test(event.key)) {\n                // Ignore all number keys from the Keypad because of a certain input method\n                // of (advance-)ASCII characters on Windows OS: ALT+[numerical code from keypad]\n                // See https://support.microsoft.com/en-us/office/insert-ascii-or-unicode-latin-based-symbols-and-characters-d13f58d3-7bcb-44a7-a4d5-972ee12e50e0#bm1\n                return;\n            }\n\n            const hotkey = getActiveHotkey(event);\n            if (!hotkey) {\n                return;\n            }\n            const { activeElement, isBlocked } = ui;\n\n            // Do not dispatch if UI is blocked\n            if (isBlocked) {\n                return;\n            }\n\n            // Replace all [accesskey] attrs by [data-hotkey] on all elements.\n            // This is needed to take over on the default accesskey behavior\n            // and also to avoid any conflict with it.\n            const elementsWithAccessKey = document.querySelectorAll(\"[accesskey]\");\n            for (const el of elementsWithAccessKey) {\n                if (el instanceof HTMLElement) {\n                    el.dataset.hotkey = el.accessKey;\n                    el.removeAttribute(\"accesskey\");\n                }\n            }\n\n            // Special case: open hotkey overlays\n            if (!overlaysVisible && hotkey === hotkeyService.overlayModifier) {\n                addHotkeyOverlays(activeElement);\n                event.preventDefault();\n                return;\n            }\n\n            // Is the pressed key NOT whitelisted ?\n            const singleKey = hotkey.split(\"+\").pop();\n            if (!AUTHORIZED_KEYS.includes(singleKey)) {\n                return;\n            }\n\n            // Protect any editable target that does not explicitly accept hotkeys\n            // NB: except for ESC, which is always allowed as hotkey in editables.\n            const targetIsEditable =\n                event.target instanceof HTMLElement &&\n                (/input|textarea/i.test(event.target.tagName) || event.target.isContentEditable) &&\n                !event.target.matches(\"input[type=checkbox], input[type=radio]\");\n            const shouldProtectEditable =\n                targetIsEditable && !event.target.dataset.allowHotkeys && singleKey !== \"escape\";\n\n            // Finally, prepare and dispatch.\n            const infos = {\n                activeElement,\n                hotkey,\n                isRepeated: event.repeat,\n                target: event.target,\n                shouldProtectEditable,\n            };\n            const dispatched = dispatch(infos);\n            if (dispatched) {\n                // Only if event has been handled.\n                // Purpose: prevent browser defaults\n                event.preventDefault();\n                // Purpose: stop other window keydown listeners (e.g. home menu)\n                event.stopImmediatePropagation();\n            }\n\n            // Finally, always remove overlays at that point\n            if (overlaysVisible) {\n                removeHotkeyOverlays();\n                event.preventDefault();\n            }\n        }\n\n        /**\n         * Dispatches an hotkey to first matching registration.\n         * Registrations are iterated in following order:\n         * - priority to all registrations done through the hotkeyService.add()\n         *   method (NB: in descending order of insertion = newer first)\n         * - then all registrations done through the DOM [data-hotkey] attribute\n         *\n         * @param {{\n         *  activeElement: HTMLElement,\n         *  hotkey: string,\n         *  isRepeated: boolean,\n         *  target: EventTarget,\n         *  shouldProtectEditable: boolean,\n         * }} infos\n         * @returns {boolean} true if has been dispatched\n         */\n        function dispatch(infos) {\n            const { activeElement, hotkey, isRepeated, target, shouldProtectEditable } = infos;\n\n            // Prepare registrations and the common filter\n            const reversedRegistrations = Array.from(registrations.values()).reverse();\n            const domRegistrations = getDomRegistrations(hotkey, activeElement);\n            const allRegistrations = reversedRegistrations.concat(domRegistrations);\n\n            // Find all candidates\n            const candidates = allRegistrations.filter(\n                (reg) =>\n                    reg.hotkey === hotkey &&\n                    (reg.allowRepeat || !isRepeated) &&\n                    (reg.bypassEditableProtection || !shouldProtectEditable) &&\n                    (reg.global || reg.activeElement === activeElement) &&\n                    (!reg.isAvailable || reg.isAvailable()) &&\n                    (!reg.area || (target && reg.area() && reg.area().contains(target)))\n            );\n\n            // First candidate\n            let winner = candidates.shift();\n            if (winner && winner.area) {\n                // If there is an area, find the closest one\n                for (const candidate of candidates.filter((c) => Boolean(c.area))) {\n                    if (candidate.area() && winner.area().contains(candidate.area())) {\n                        winner = candidate;\n                    }\n                }\n            }\n\n            // Dispatch actual hotkey to the matching registration\n            if (winner) {\n                winner.callback({\n                    area: winner.area && winner.area(),\n                    target,\n                });\n                return true;\n            }\n            return false;\n        }\n\n        /**\n         * Get a list of registrations from the [data-hotkey] defined in the DOM\n         *\n         * @param {string} hotkey\n         * @param {HTMLElement} activeElement\n         * @returns {HotkeyRegistration[]}\n         */\n        function getDomRegistrations(hotkey, activeElement) {\n            const overlayModParts = hotkeyService.overlayModifier.split(\"+\");\n            if (!overlayModParts.every((el) => hotkey.includes(el))) {\n                return [];\n            }\n\n            // Get all elements having a data-hotkey attribute  and matching\n            // the actual hotkey without the overlayModifier.\n            const cleanHotkey = hotkey\n                .split(\"+\")\n                .filter((key) => !overlayModParts.includes(key))\n                .join(\"+\");\n            const elems = getVisibleElements(activeElement, `[data-hotkey='${cleanHotkey}' i]`);\n            return elems.map((el) => ({\n                hotkey,\n                activeElement,\n                bypassEditableProtection: true,\n                callback: () => {\n                    // AAB: not sure it is enough, we might need to trigger all events that occur when you actually click\n                    el.focus();\n                    el.click();\n                },\n            }));\n        }\n\n        /**\n         * Add the hotkey overlays respecting the ui active element.\n         * @param {HTMLElement} activeElement\n         */\n        function addHotkeyOverlays(activeElement) {\n            // Gather the hotkeys to overlay registered through the useHotkey hook.\n            const hotkeysFromHookToHighlight = [];\n            for (const [, registration] of registrations) {\n                const overlayElement = registration.withOverlay?.();\n                if (overlayElement) {\n                    hotkeysFromHookToHighlight.push({\n                        hotkey: registration.hotkey.replace(\n                            `${hotkeyService.overlayModifier}+`,\n                            \"\"\n                        ),\n                        el: overlayElement,\n                    });\n                }\n            }\n\n            // Gather the hotkeys to overlay registered through the DOM datasets.\n            const hotkeysFromDomToHighlight = getVisibleElements(\n                activeElement,\n                \"[data-hotkey]:not(:disabled)\"\n            ).map((el) => ({ hotkey: el.dataset.hotkey, el }));\n\n            const items = [...hotkeysFromDomToHighlight, ...hotkeysFromHookToHighlight];\n            for (const item of items) {\n                const hotkey = item.hotkey;\n                const overlay = document.createElement(\"div\");\n                overlay.classList.add(\n                    \"o_web_hotkey_overlay\",\n                    \"position-absolute\",\n                    \"top-0\",\n                    \"bottom-0\",\n                    \"start-0\",\n                    \"end-0\",\n                    \"d-flex\",\n                    \"justify-content-center\",\n                    \"align-items-center\",\n                    \"m-0\",\n                    \"bg-black-50\",\n                    \"h6\"\n                );\n                overlay.style.zIndex = 1;\n                const overlayKbd = document.createElement(\"kbd\");\n                overlayKbd.className = \"small\";\n                overlayKbd.appendChild(document.createTextNode(hotkey.toUpperCase()));\n                overlay.appendChild(overlayKbd);\n\n                let overlayParent;\n                if (item.el.tagName.toUpperCase() === \"INPUT\") {\n                    // special case for the search input that has an access key\n                    // defined. We cannot set the overlay on the input itself,\n                    // only on its parent.\n                    overlayParent = item.el.parentElement;\n                } else {\n                    overlayParent = item.el;\n                }\n\n                if (overlayParent.style.position !== \"absolute\") {\n                    overlayParent.style.position = \"relative\";\n                }\n                overlayParent.appendChild(overlay);\n            }\n            overlaysVisible = true;\n        }\n\n        /**\n         * Remove all the hotkey overlays.\n         */\n        function removeHotkeyOverlays() {\n            for (const overlay of document.querySelectorAll(\".o_web_hotkey_overlay\")) {\n                overlay.remove();\n            }\n            overlaysVisible = false;\n        }\n\n        /**\n         * Registers a new hotkey.\n         *\n         * @param {string} hotkey\n         * @param {HotkeyCallback} callback\n         * @param {HotkeyOptions} [options]\n         * @returns {number} registration token\n         */\n        function registerHotkey(hotkey, callback, options = {}) {\n            // Validate some informations\n            if (!hotkey || hotkey.length === 0) {\n                throw new Error(\"You must specify an hotkey when registering a registration.\");\n            }\n\n            if (!callback || typeof callback !== \"function\") {\n                throw new Error(\n                    \"You must specify a callback function when registering a registration.\"\n                );\n            }\n\n            /**\n             * An hotkey must comply to these rules:\n             *  - all parts are whitelisted\n             *  - single key part comes last\n             *  - each part is separated by the dash character: \"+\"\n             */\n            const keys = hotkey\n                .toLowerCase()\n                .split(\"+\")\n                .filter((k) => !MODIFIERS.includes(k));\n            if (keys.some((k) => !AUTHORIZED_KEYS.includes(k))) {\n                throw new Error(\n                    `You are trying to subscribe for an hotkey ('${hotkey}')\n            that contains parts not whitelisted: ${keys.join(\", \")}`\n                );\n            } else if (keys.length > 1) {\n                throw new Error(\n                    `You are trying to subscribe for an hotkey ('${hotkey}')\n            that contains more than one single key part: ${keys.join(\"+\")}`\n                );\n            }\n\n            // Add registration\n            const token = nextToken++;\n            /** @type {HotkeyRegistration} */\n            const registration = {\n                hotkey: hotkey.toLowerCase(),\n                callback,\n                activeElement: null,\n                allowRepeat: options && options.allowRepeat,\n                bypassEditableProtection: options && options.bypassEditableProtection,\n                global: options && options.global,\n                area: options && options.area,\n                isAvailable: options && options.isAvailable,\n                withOverlay: options && options.withOverlay,\n            };\n\n            // Due to the way elements are mounted in the DOM by Owl (bottom-to-top),\n            // we need to wait the next micro task tick to set the context owner of the registration.\n            Promise.resolve().then(() => {\n                registration.activeElement = ui.activeElement;\n            });\n\n            registrations.set(token, registration);\n            return token;\n        }\n\n        /**\n         * Unsubscribes the token corresponding registration.\n         *\n         * @param {number} token\n         */\n        function unregisterHotkey(token) {\n            registrations.delete(token);\n        }\n\n        return {\n            /**\n             * @param {string} hotkey\n             * @param {HotkeyCallback} callback\n             * @param {HotkeyOptions} [options]\n             * @returns {() => void}\n             */\n            add(hotkey, callback, options = {}) {\n                const token = registerHotkey(hotkey, callback, options);\n                return () => {\n                    unregisterHotkey(token);\n                };\n            },\n            /**\n             * @param {HTMLIFrameElement} iframe\n             */\n            registerIframe(iframe) {\n                addListeners(iframe.contentWindow);\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"hotkey\", hotkeyService);\n/** @typedef {ReturnType<hotkeyService[\"start\"]>} HotkeyService */\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { Component, onMounted, useState } from \"@odoo/owl\";\nimport { isDisplayStandalone } from \"@web/core/browser/feature_detection\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\n\nexport class InstallScopedApp extends Component {\n    static props = {};\n    static template = \"web.InstallScopedApp\";\n    static components = { Dropdown };\n    setup() {\n        this.pwa = useState(useService(\"pwa\"));\n        this.state = useState({ manifest: {}, showInstallUI: false });\n        this.isDisplayStandalone = isDisplayStandalone();\n        // BeforeInstallPrompt event can take while before the browser triggers it. Some will display\n        // immediately, others will wait that the user has interacted for some time with the website.\n        this.isInstallationPossible = browser.BeforeInstallPromptEvent !== undefined;\n        onMounted(async () => {\n            this.state.manifest = await this.pwa.getManifest();\n            this.state.showInstallUI = true;\n        });\n    }\n    onChangeName(ev) {\n        const value = ev.target.value;\n        if (value !== this.state.manifest.name) {\n            const url = new URL(document.location.href);\n            url.searchParams.set(\"app_name\", encodeURIComponent(value));\n            browser.location.replace(url);\n        }\n    }\n    onInstall() {\n        this.state.showInstallUI = false;\n        this.pwa.show({\n            onDone: (res) => {\n                if (res.outcome === \"accepted\") {\n                    browser.location.replace(this.state.manifest.start_url);\n                } else {\n                    this.state.showInstallUI = true;\n                }\n            },\n        });\n    }\n}\n\nregistry.category(\"public_components\").add(\"web.install_scoped_app\", InstallScopedApp);\n", "import { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { ensureArray } from \"../utils/arrays\";\n\nconst { DateTime, Settings } = luxon;\n\n/**\n * @typedef ConversionOptions\n *  This is a list of the available options to either:\n *  - convert a DateTime to a string (format)\n *  - convert a string to a DateTime (parse)\n *  All of these are optional and the default values are issued by the Localization service.\n *\n * @property {string} [format]\n *  Format used to format a DateTime or to parse a formatted string.\n *  > Default: the session localization format.\n * @property {boolean} [condensed] if true, months, days and hours will be formatted without\n *  leading 0.\n *\n * @typedef {luxon.DateTime} DateTime\n *\n * @typedef {[NullableDateTime, NullableDateTime]} NullableDateRange\n *\n * @typedef {DateTime | false | null | undefined} NullableDateTime\n */\n\n/**\n * Limits defining a valid date.\n * This is needed because the server only understands 4-digit years.\n * Note: both of these are in the local timezone\n */\nexport const MIN_VALID_DATE = DateTime.fromObject({ year: 1000 });\nexport const MAX_VALID_DATE = DateTime.fromObject({ year: 9999 }).endOf(\"year\");\n\nconst SERVER_DATE_FORMAT = \"yyyy-MM-dd\";\nconst SERVER_TIME_FORMAT = \"HH:mm:ss\";\nconst SERVER_DATETIME_FORMAT = `${SERVER_DATE_FORMAT} ${SERVER_TIME_FORMAT}`;\n\nconst nonAlphaRegex = /[^a-z]/gi;\nconst nonDigitRegex = /[^\\d]/g;\n\nconst normalizeFormatTable = {\n    // Python strftime to luxon.js conversion table\n    // See odoo/addons/base/views/res_lang_views.xml\n    // for details about supported directives\n    a: \"ccc\",\n    A: \"cccc\",\n    b: \"MMM\",\n    B: \"MMMM\",\n    d: \"dd\",\n    H: \"HH\",\n    I: \"hh\",\n    j: \"o\",\n    m: \"MM\",\n    M: \"mm\",\n    p: \"a\",\n    S: \"ss\",\n    W: \"WW\",\n    w: \"c\",\n    y: \"yy\",\n    Y: \"yyyy\",\n    c: \"ccc MMM d HH:mm:ss yyyy\",\n    x: \"MM/dd/yy\",\n    X: \"HH:mm:ss\",\n};\n\nconst smartDateUnits = {\n    d: \"days\",\n    m: \"months\",\n    w: \"weeks\",\n    y: \"years\",\n};\nconst smartDateRegex = new RegExp(\n    [\"^\", \"([+-])\", \"(\\\\d+)\", `([${Object.keys(smartDateUnits).join(\"\")}]?)`, \"$\"].join(\"\\\\s*\"),\n    \"i\"\n);\n\n/** @type {WeakMap<DateTime, string>} */\nconst dateCache = new WeakMap();\n/** @type {WeakMap<DateTime, string>} */\nconst dateTimeCache = new WeakMap();\n\nexport class ConversionError extends Error {\n    name = \"ConversionError\";\n}\n\n//-----------------------------------------------------------------------------\n// Helpers\n//-----------------------------------------------------------------------------\n\n/**\n * Checks whether 2 given dates or date ranges are equal. Both values are allowed\n * to be falsy or to not be of the same type (which will return false).\n *\n * @param {NullableDateTime | NullableDateRange} d1\n * @param {NullableDateTime | NullableDateRange} d2\n * @returns {boolean}\n */\nexport function areDatesEqual(d1, d2) {\n    if (Array.isArray(d1) || Array.isArray(d2)) {\n        // One of the values is a date range -> checks deep equality between the ranges\n        d1 = ensureArray(d1);\n        d2 = ensureArray(d2);\n        return d1.length === d2.length && d1.every((d1Val, i) => areDatesEqual(d1Val, d2[i]));\n    }\n    if (d1 instanceof DateTime && d2 instanceof DateTime && d1 !== d2) {\n        // Both values are DateTime objects -> use Luxon's comparison\n        return d1.equals(d2);\n    } else {\n        // One of the values is not a DateTime object -> fallback to strict equal\n        return d1 === d2;\n    }\n}\n\n/**\n * @param {DateTime} desired\n * @param {DateTime} minDate\n * @param {DateTime} maxDate\n */\nexport function clampDate(desired, minDate, maxDate) {\n    if (maxDate < desired) {\n        return maxDate;\n    }\n    if (minDate > desired) {\n        return minDate;\n    }\n    return desired;\n}\n\n/**\n * Get the week number of a given date.\n * Returns the ISO week number of the Monday nearest to the first day of the week.\n *\n * @param {Date | luxon.DateTime} date\n * @returns {number}\n *  the ISO week number (1-53) of the nearest Monday of the given date's week's start\n */\nexport function getLocalWeekNumber(date) {\n    if (!date.isLuxonDateTime) {\n        date = DateTime.fromJSDate(date);\n    }\n    const { weekStart } = localization;\n    // go to start of week\n    date = date.minus({ days: (date.weekday + 7 - weekStart) % 7 });\n    // go to nearest Monday, up to 3 days back- or forwards\n    date =\n        weekStart > 1 && weekStart < 5 // if firstDay after Mon & before Fri\n            ? date.minus({ days: (date.weekday + 6) % 7 }) // then go back 1-3 days\n            : date.plus({ days: (8 - date.weekday) % 7 }); // else go forwards 0-3 days\n    date = date.plus({ days: 6 }); // go to last weekday of ISO week\n    const jan4 = DateTime.local(date.year, 1, 4);\n    // count from previous year if week falls before Jan 4\n    const diffDays =\n        date < jan4 ? date.diff(jan4.minus({ years: 1 }), \"day\").days : date.diff(jan4, \"day\").days;\n    return Math.trunc(diffDays / 7) + 1;\n}\n\n/**\n * Get the start of the week for the given date, in the user's locale settings.\n * The start of the week is determined by the `weekStart` setting.\n *\n * Luxon's `.startOf(\"week\")` method uses the ISO week definition, which starts on Monday.\n * Luxon has a `.startOf(\"week\", { useLocaleWeeks: true })` method, but it relies on the\n * Intl API and the `getWeekInfo` method, which is not supported in all browsers.\n * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo#browser_compatibility\n *\n * @param {luxon.DateTime} date\n * @returns {luxon.DateTime}\n */\nexport function getStartOfLocalWeek(date) {\n    const { weekStart } = localization;\n    const weekday = date.weekday < weekStart ? weekStart - 7 : weekStart;\n    return date.set({ weekday }).startOf(\"day\");\n}\n\n/**\n * Get the end of the week for the given date, in the user's locale settings.\n * The end of the week is determined by the `weekStart` setting.\n *\n * Luxon's `.endOf(\"week\")` method uses the ISO week definition, which starts on Monday.\n * Luxon has a `.endOf(\"week\", { useLocaleWeeks: true })` method, but it relies on the\n * Intl API and the `getWeekInfo` method, which is not supported in all browsers.\n * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo#browser_compatibility\n *\n * @param {luxon.DateTime} date\n * @returns {luxon.DateTime}\n */\nexport function getEndOfLocalWeek(date) {\n    return getStartOfLocalWeek(date).plus({ days: 6 }).endOf(\"day\");\n}\n\n/**\n * Returns whether the given format is a 24-hour format.\n * Falls back to localization time format if none is given.\n *\n * @param {string} format\n */\nexport function is24HourFormat(format) {\n    return /H/.test(format || localization.timeFormat);\n}\n\n/**\n * @param {NullableDateTime | NullableDateRange} value\n * @param {NullableDateRange} range\n * @returns {boolean}\n */\nexport function isInRange(value, range) {\n    if (!value || !range) {\n        return false;\n    }\n    if (Array.isArray(value)) {\n        const actualValues = value.filter(Boolean);\n        if (actualValues.length < 2) {\n            return isInRange(actualValues[0], range);\n        }\n        return (\n            (value[0] <= range[0] && range[0] <= value[1]) ||\n            (range[0] <= value[0] && value[0] <= range[1])\n        );\n    } else {\n        return range[0] <= value && value <= range[1];\n    }\n}\n\n/**\n * Returns whether the given format uses a meridiem suffix (AM/PM).\n * Falls back to localization time format if none is given.\n *\n * @param {string} format\n */\nexport function isMeridiemFormat(format) {\n    return /a/.test(format || localization.timeFormat);\n}\n\n/**\n * Returns whether the given DateTime is valid.\n * The date is considered valid if it:\n * - is a DateTime object\n * - has the \"isValid\" flag set to true\n * - is between 1000-01-01 and 9999-12-31 (both included)\n * @see MIN_VALID_DATE\n * @see MAX_VALID_DATE\n *\n * @param {NullableDateTime} date\n */\nfunction isValidDate(date) {\n    return date && date.isValid && isInRange(date, [MIN_VALID_DATE, MAX_VALID_DATE]);\n}\n\n/**\n * Smart date inputs are shortcuts to write dates quicker.\n * These shortcuts should respect the format ^[+-]\\d+[dmwy]?$\n *\n * e.g.\n *   \"+1d\" or \"+1\" will return now + 1 day\n *   \"-2w\" will return now - 2 weeks\n *   \"+3m\" will return now + 3 months\n *   \"-4y\" will return now + 4 years\n *\n * @param {string} value\n * @returns {NullableDateTime} Luxon datetime object (in the user's local timezone)\n */\nfunction parseSmartDateInput(value) {\n    const match = value.match(smartDateRegex);\n    if (match) {\n        let date = DateTime.local();\n        const offset = parseInt(match[2], 10);\n        const unit = smartDateUnits[(match[3] || \"d\").toLowerCase()];\n        if (match[1] === \"+\") {\n            date = date.plus({ [unit]: offset });\n        } else {\n            date = date.minus({ [unit]: offset });\n        }\n        return date;\n    }\n    return false;\n}\n\n/**\n * Removes any duplicate *subsequent* alphabetic characters in a given string.\n * Example: \"aa-bb-CCcc-ddD-c xxxx-Yy-ZZ\" -> \"a-b-Cc-dD-c x-Yy-Z\"\n *\n * @type {(str: string) => string}\n */\nconst stripAlphaDupes = memoize(function stripAlphaDupes(str) {\n    return str.replace(/[a-z]/gi, (letter, index, str) =>\n        letter === str[index - 1] ? \"\" : letter\n    );\n});\n\n/**\n * Convert Python strftime to escaped luxon.js format.\n *\n * @type {(format: string) => string}\n */\nexport const strftimeToLuxonFormat = memoize(function strftimeToLuxonFormat(format) {\n    const output = [];\n    let inToken = false;\n    for (let index = 0; index < format.length; ++index) {\n        let character = format[index];\n        if (character === \"%\" && !inToken) {\n            inToken = true;\n            continue;\n        }\n        if (/[a-z]/gi.test(character)) {\n            if (inToken && normalizeFormatTable[character] !== undefined) {\n                character = normalizeFormatTable[character];\n            } else {\n                character = `'${character}'`; // luxon escape\n            }\n        }\n        output.push(character);\n        inToken = false;\n    }\n    return output.join(\"\");\n});\n\n/**\n * Lazy getter returning the start of the current day.\n */\nexport function today() {\n    return DateTime.local().startOf(\"day\");\n}\n\n//-----------------------------------------------------------------------------\n// Formatting\n//-----------------------------------------------------------------------------\n\nconst condensedFormats = {};\n/**\n * Given a date(time) format, returns a format where months, days and hours are\n * displayed without the leading 0 (e.g. 03/05/2024 08:00:00 => 3/5/2024 8:00:00).\n *\n * @param {string} format\n * @returns string\n */\nfunction getCondensedFormat(format) {\n    const originalFormat = format;\n    if (!condensedFormats[originalFormat]) {\n        format = format.replace(/(^|[^M])M{2}([^M]|$)/, \"$1M$2\");\n        format = format.replace(/(^|[^d])d{2}([^d]|$)/, \"$1d$2\");\n        format = format.replace(/(^|[^H])H{2}([^H]|$)/, \"$1H$2\");\n        condensedFormats[originalFormat] = format;\n    }\n    return condensedFormats[originalFormat];\n}\n\n/**\n * Formats a DateTime object to a date string\n *\n * @param {NullableDateTime} value\n * @param {ConversionOptions} [options={}]\n */\nexport function formatDate(value, options = {}) {\n    if (!value) {\n        return \"\";\n    }\n    let format = options.format;\n    if (!format) {\n        format = localization.dateFormat;\n        if (options.condensed) {\n            format = getCondensedFormat(format);\n        }\n    }\n    return value.toFormat(format);\n}\n\n/**\n * Formats a DateTime object to a datetime string\n *\n * @param {NullableDateTime} value\n * @param {ConversionOptions} [options={}]\n */\nexport function formatDateTime(value, options = {}) {\n    if (!value) {\n        return \"\";\n    }\n    let format = options.format;\n    if (!format) {\n        if (options.showSeconds === false) {\n            format = `${localization.dateFormat} ${localization.shortTimeFormat}`;\n        } else {\n            format = localization.dateTimeFormat;\n        }\n        if (options.condensed) {\n            format = getCondensedFormat(format);\n        }\n    }\n    return value.setZone(options.tz || \"default\").toFormat(format);\n}\n\n/**\n * Converts a given duration in seconds into a human-readable format.\n *\n * The function takes a duration in seconds and converts it into a human-readable form,\n * such as \"1h\" or \"1 hour, 30 minutes\", depending on the value of the `showFullDuration` parameter.\n * If the `showFullDuration` is set to true, the function will display up to two non-zero duration\n * components in long form (e.g: hours, minutes).\n * Otherwise, it will show just the largest non-zero duration component in narrow form (e.g: y or h).\n * Luxon takes care of translations given the current locale.\n *\n * @param {number} seconds - The duration in seconds to be converted.\n * @param {boolean} showFullDuration - If true, the output will have two components in long form.\n * Otherwise, just one component will be displayed in narrow form.\n *\n * @returns {string} A human-readable string representation of the duration.\n *\n * @example\n * // Sample usage\n * const durationInSeconds = 7320; // 2 hours and 2 minutes (2 * 3600 + 2 * 60)\n * const fullDuration = humanizeDuration(durationInSeconds, true);\n * console.log(fullDuration); // Output: \"2 hours, 2 minutes\"\n *\n * const shortDuration = humanizeDuration(durationInSeconds, false);\n * console.log(shortDuration); // Output: \"2h\"\n */\nexport function formatDuration(seconds, showFullDuration) {\n    const displayStyle = showFullDuration ? \"long\" : \"narrow\";\n    const numberOfValuesToDisplay = showFullDuration ? 2 : 1;\n    const durationKeys = [\"years\", \"months\", \"days\", \"hours\", \"minutes\"];\n\n    if (seconds < 60) {\n        seconds = 60;\n    }\n    seconds -= seconds % 60;\n\n    let duration = luxon.Duration.fromObject({ seconds: seconds }).shiftTo(...durationKeys);\n    duration = duration.shiftTo(...durationKeys.filter((key) => duration.get(key)));\n    const durationSplit = duration.toHuman({ unitDisplay: displayStyle }).split(\",\");\n\n    if (!showFullDuration && duration.loc.locale.includes(\"en\") && duration.months > 0) {\n        durationSplit[0] = durationSplit[0].replace(\"m\", \"M\");\n    }\n    return durationSplit.slice(0, numberOfValuesToDisplay).join(\",\");\n}\n\n/**\n * Formats the given DateTime to the server date format.\n * @param {DateTime} value\n * @returns {string}\n */\nexport function serializeDate(value) {\n    if (!dateCache.has(value)) {\n        dateCache.set(value, value.toFormat(SERVER_DATE_FORMAT, { numberingSystem: \"latn\" }));\n    }\n    return dateCache.get(value);\n}\n\n/**\n * Formats the given DateTime to the server datetime format.\n * @param {DateTime} value\n * @returns {string}\n */\nexport function serializeDateTime(value) {\n    if (!dateTimeCache.has(value)) {\n        dateTimeCache.set(\n            value,\n            value.setZone(\"utc\").toFormat(SERVER_DATETIME_FORMAT, { numberingSystem: \"latn\" })\n        );\n    }\n    return dateTimeCache.get(value);\n}\n\n//-----------------------------------------------------------------------------\n// Parsing\n//-----------------------------------------------------------------------------\n\n/**\n * Parses a string value to a Luxon DateTime object.\n *\n * @param {string} value\n * @param {ConversionOptions} [options={}]\n *\n * @see parseDateTime (Note: since we're only interested by the date itself, the\n *  returned value will always be set at the start of the day)\n */\nexport function parseDate(value, options = {}) {\n    const parsed = parseDateTime(value, {\n        ...options,\n        format: options.format || localization.dateFormat,\n    });\n    return parsed && parsed.startOf(\"day\");\n}\n\n/**\n * Parses a string value to a Luxon DateTime object.\n *\n * @param {string} value value to parse.\n *  - Value can take the form of a smart date:\n *    e.g. \"+3w\" for three weeks from now.\n *    (`options.format` is ignored in this case)\n *\n *  - If value cannot be parsed within the provided format,\n *    ISO8601 and SQL formats are then tried. If these formats\n *    include a timezone information, the returned value will\n *    still be set to the user's timezone.\n *    e.g. \"2020-01-01T12:00:00+06:00\" with the user's timezone being UTC+1,\n *         the returned value will express the same timestamp but in UTC+1 (here time will be 7:00).\n *\n * @param {ConversionOptions} options\n *\n * @returns {NullableDateTime} Luxon DateTime object in user's timezone\n */\nexport function parseDateTime(value, options = {}) {\n    if (!value) {\n        return false;\n    }\n\n    const fmt = options.format || localization.dateTimeFormat;\n    const parseOpts = {\n        setZone: true,\n        zone: options.tz || \"default\",\n    };\n    const switchToLatin = Settings.defaultNumberingSystem !== \"latn\" && /[0-9]/.test(value);\n\n    // Force numbering system to latin if actual numbers are found in the value\n    if (switchToLatin) {\n        parseOpts.numberingSystem = \"latn\";\n    }\n\n    // Base case: try parsing with the given format and options\n    let result = DateTime.fromFormat(value, fmt, parseOpts);\n\n    // Try parsing as a smart date\n    if (!isValidDate(result)) {\n        result = parseSmartDateInput(value);\n    }\n\n    // Try parsing with partial date parts\n    if (!isValidDate(result)) {\n        const fmtWoZero = stripAlphaDupes(fmt);\n        result = DateTime.fromFormat(value, fmtWoZero, parseOpts);\n    }\n\n    // Try parsing with custom shorthand date parts\n    if (!isValidDate(result)) {\n        // Luxon is not permissive regarding delimiting characters in the format.\n        // So if the value to parse has less characters than the format, we would\n        // try to parse without the delimiting characters.\n        const digitList = value.split(nonDigitRegex).filter(Boolean);\n        const fmtList = fmt.split(nonAlphaRegex).filter(Boolean);\n        const valWoSeps = digitList.join(\"\");\n\n        // This is the weird part: we try to adapt the given format to comply with\n        // the amount of digits in the given value. To do this we split the format\n        // and the value on non-letter and non-digit characters respectively. This\n        // should create the same amount of grouping parameters, and the format\n        // groups are trimmed according to the length of their corresponding\n        // digit group. The 'carry' variable allows for the length of a digit\n        // group to overflow to the next format group. This is typically the case\n        // when the given value doesn't have non-digit separators and generates\n        // one big digit group instead.\n        let carry = 0;\n        const fmtWoSeps = fmtList\n            .map((part, i) => {\n                const digitLength = (digitList[i] || \"\").length;\n                const actualPart = part.slice(0, digitLength + carry);\n                carry += digitLength - actualPart.length;\n                return actualPart;\n            })\n            .join(\"\");\n\n        result = DateTime.fromFormat(valWoSeps, fmtWoSeps, parseOpts);\n    }\n\n    // Try with defaul ISO or SQL formats\n    if (!isValidDate(result)) {\n        // Also try some fallback formats, but only if value counts more than\n        // four digit characters as this could get misinterpreted as the time of\n        // the actual date.\n        const valueDigits = value.replace(nonDigitRegex, \"\");\n        if (valueDigits.length > 4) {\n            result = DateTime.fromISO(value, parseOpts); // ISO8601\n            if (!isValidDate(result)) {\n                result = DateTime.fromSQL(value, parseOpts); // last try: SQL\n            }\n        }\n    }\n\n    // No working parsing methods: throw an error\n    if (!isValidDate(result)) {\n        throw new ConversionError(_t(\"'%s' is not a correct date or datetime\", value));\n    }\n\n    // Revert to original numbering system\n    if (switchToLatin) {\n        result = result.reconfigure({\n            numberingSystem: Settings.defaultNumberingSystem,\n        });\n    }\n\n    return result.setZone(options.tz || \"default\");\n}\n\n/**\n * Returns a date object parsed from the given serialized string.\n * @param {string} value serialized date string, e.g. \"2018-01-01\"\n */\nexport function deserializeDate(value, options = {}) {\n    options = { numberingSystem: \"latn\", zone: \"default\", ...options };\n    return DateTime.fromSQL(value, options).reconfigure({\n        numberingSystem: Settings.defaultNumberingSystem,\n    });\n}\n\n/**\n * Returns a datetime object parsed from the given serialized string.\n * @param {string} value serialized datetime string, e.g. \"2018-01-01 00:00:00\", expressed in UTC\n */\nexport function deserializeDateTime(value, options = {}) {\n    return DateTime.fromSQL(value, { numberingSystem: \"latn\", zone: \"utc\" })\n        .setZone(options?.tz || \"default\")\n        .reconfigure({\n            numberingSystem: Settings.defaultNumberingSystem,\n        });\n}\n", "/**\n * @typedef Localization\n * @property {string} dateFormat\n * @property {string} dateTimeFormat\n * @property {string} timeFormat\n * @property {string} decimalPoint\n * @property {\"ltr\" | \"rtl\"} direction\n * @property {[number, number]} grouping\n * @property {boolean} multiLang\n * @property {string} thousandsSep\n * @property {number} weekStart\n * @property {string} code\n */\n\n/**\n * This is the main object holding user specific data about the localization. Its basically\n * the JS counterpart of the \"res.lang\" model.\n * It is useful to directly access those data anywhere, even outside Components.\n *\n * Important Note: its data are actually loaded by the localization_service,\n * so a code like the following would not work:\n *   import { localization } from \"@web/core/l10n/localization\";\n *   const dateFormat = localization.dateFormat; // dateFormat isn't set yet\n * @type {Localization}\n */\nexport const localization = new Proxy(\n    {},\n    {\n        get: (target, p) => {\n            // \"then\" can be called implicitly if the object is returned in an\n            // `async` function, so we need to allow it.\n            if (p in target || p === \"then\") {\n                return Reflect.get(target, p);\n            }\n            throw new Error(\n                `could not access localization parameter \"${p}\": parameters are not ready yet. Maybe add 'localization' to your dependencies?`\n            );\n        },\n    }\n);\n", "import { session } from \"@web/session\";\nimport { jsToPyLocale } from \"@web/core/l10n/utils\";\nimport { user } from \"@web/core/user\";\nimport { browser } from \"../browser/browser\";\nimport { registry } from \"../registry\";\nimport { strftimeToLuxonFormat } from \"./dates\";\nimport { localization } from \"./localization\";\nimport { translatedTerms, translationLoaded, translationIsReady } from \"./translation\";\n\nconst { Settings } = luxon;\n\n/** @type {[RegExp, string][]} */\nconst NUMBERING_SYSTEMS = [\n    [/^ar-(sa|sy|001)$/i, \"arab\"],\n    [/^bn/i, \"beng\"],\n    [/^bo/i, \"tibt\"],\n    // [/^fa/i, \"Farsi (Persian)\"], // No numberingSystem found in Intl\n    // [/^(hi|mr|ne)/i, \"Hindi\"], // No numberingSystem found in Intl\n    // [/^my/i, \"Burmese\"], // No numberingSystem found in Intl\n    [/^pa-in/i, \"guru\"],\n    [/^ta/i, \"tamldec\"],\n    [/.*/i, \"latn\"],\n];\n\nexport const localizationService = {\n    start: async () => {\n        const cacheHashes = session.cache_hashes || {};\n        const translationsHash = cacheHashes.translations || new Date().getTime().toString();\n        const lang = jsToPyLocale(user.lang || document.documentElement.getAttribute(\"lang\"));\n        const translationURL = session.translationURL || \"/web/webclient/translations\";\n        let url = `${translationURL}/${translationsHash}`;\n        if (lang) {\n            url += `?lang=${lang}`;\n        }\n\n        const response = await browser.fetch(url);\n        if (!response.ok) {\n            throw new Error(\"Error while fetching translations\");\n        }\n\n        const {\n            lang_parameters: userLocalization,\n            modules: modules,\n            multi_lang: multiLang,\n        } = await response.json();\n\n        // FIXME We flatten the result of the python route.\n        // Eventually, we want a new python route to return directly the good result.\n        const terms = {};\n        for (const addon of Object.keys(modules)) {\n            for (const message of modules[addon].messages) {\n                terms[message.id] = message.string;\n            }\n        }\n\n        Object.assign(translatedTerms, terms);\n        translatedTerms[translationLoaded] = true;\n        translationIsReady.resolve(true);\n\n        const locale = user.lang || browser.navigator.language;\n        Settings.defaultLocale = locale;\n        for (const [re, numberingSystem] of NUMBERING_SYSTEMS) {\n            if (re.test(locale)) {\n                Settings.defaultNumberingSystem = numberingSystem;\n                break;\n            }\n        }\n\n        const dateFormat = strftimeToLuxonFormat(userLocalization.date_format);\n        const timeFormat = strftimeToLuxonFormat(userLocalization.time_format);\n        const shortTimeFormat = strftimeToLuxonFormat(userLocalization.short_time_format);\n        const dateTimeFormat = `${dateFormat} ${timeFormat}`;\n        const grouping = JSON.parse(userLocalization.grouping);\n\n        Object.assign(localization, {\n            dateFormat,\n            timeFormat,\n            shortTimeFormat,\n            dateTimeFormat,\n            decimalPoint: userLocalization.decimal_point,\n            direction: userLocalization.direction,\n            grouping,\n            multiLang,\n            thousandsSep: userLocalization.thousands_sep,\n            weekStart: userLocalization.week_start,\n            code: jsToPyLocale(locale),\n        });\n        return localization;\n    },\n};\n\nregistry.category(\"services\").add(\"localization\", localizationService);\n", "import { Deferred } from \"@web/core/utils/concurrency\";\nimport { sprintf } from \"@web/core/utils/strings\";\n\nexport const translationLoaded = Symbol(\"translationLoaded\");\nexport const translatedTerms = {\n    [translationLoaded]: false,\n};\nexport const translationIsReady = new Deferred();\n/**\n * Translate a term, or return the term if no translation can be found.\n *\n * @param {string} term\n * @returns {string|LazyTranslatedString}\n */\nexport function _t(term, ...values) {\n    if (translatedTerms[translationLoaded]) {\n        const translation = translatedTerms[term] ?? term;\n        if (values.length === 0) {\n            return translation;\n        }\n        return sprintf(translation, ...values);\n    } else {\n        return new LazyTranslatedString(term, ...values);\n    }\n}\n\nclass LazyTranslatedString extends String {\n    constructor(term, ...values) {\n        super(term);\n        this.values = values;\n    }\n    valueOf() {\n        const term = super.valueOf();\n        if (translatedTerms[translationLoaded]) {\n            const translation = translatedTerms[term] ?? term;\n            if (this.values.length === 0) {\n                return translation;\n            }\n            return sprintf(translation, ...this.values);\n        } else {\n            throw new Error(`translation error`);\n        }\n    }\n    toString() {\n        return this.valueOf();\n    }\n}\n\n/*\n * Setup jQuery timeago:\n * Strings in timeago are \"composed\" with prefixes, words and suffixes. This\n * makes their detection by our translating system impossible. Use all literal\n * strings we're using with a translation mark here so the extractor can do its\n * job.\n */\n_t(\"less than a minute ago\");\n_t(\"about a minute ago\");\n_t(\"%d minutes ago\");\n_t(\"about an hour ago\");\n_t(\"%d hours ago\");\n_t(\"a day ago\");\n_t(\"%d days ago\");\n_t(\"about a month ago\");\n_t(\"%d months ago\");\n_t(\"about a year ago\");\n_t(\"%d years ago\");\n\n/**\n * Load the installed languages long names and code\n *\n * The result of the call is put in cache.\n * If any new language is installed, a full page refresh will happen,\n * so there is no need invalidate it.\n */\nexport async function loadLanguages(orm) {\n    if (!loadLanguages.installedLanguages) {\n        loadLanguages.installedLanguages = await orm.call(\"res.lang\", \"get_installed\");\n    }\n    return loadLanguages.installedLanguages;\n}\n", "export * from \"@web/core/l10n/utils/format_list\";\nexport * from \"@web/core/l10n/utils/locales\";\n", "import { user } from \"@web/core/user\";\n\n/**\n * Convert Unicode TR35-49 list pattern types to ES Intl.ListFormat options\n */\nconst LIST_STYLES = {\n    standard: {\n        type: \"conjunction\",\n        style: \"long\",\n    },\n    \"standard-short\": {\n        type: \"conjunction\",\n        style: \"short\",\n    },\n    or: {\n        type: \"disjunction\",\n        style: \"long\",\n    },\n    \"or-short\": {\n        type: \"disjunction\",\n        style: \"short\",\n    },\n    unit: {\n        type: \"unit\",\n        style: \"long\",\n    },\n    \"unit-short\": {\n        type: \"unit\",\n        style: \"short\",\n    },\n    \"unit-narrow\": {\n        type: \"unit\",\n        style: \"narrow\",\n    },\n};\n\n/**\n * Format the items in `list` as a list in a locale-dependent manner with the chosen style.\n *\n * The available styles are defined in the Unicode TR35-49 spec:\n * * standard:\n *   A typical \"and\" list for arbitrary placeholders.\n *   e.g. \"January, February, and March\"\n * * standard-short:\n *   A short version of an \"and\" list, suitable for use with short or abbreviated placeholder values.\n *   e.g. \"Jan., Feb., and Mar.\"\n * * or:\n *   A typical \"or\" list for arbitrary placeholders.\n *   e.g. \"January, February, or March\"\n * * or-short:\n *   A short version of an \"or\" list.\n *   e.g. \"Jan., Feb., or Mar.\"\n * * unit:\n *   A list suitable for wide units.\n *   e.g. \"3 feet, 7 inches\"\n * * unit-short:\n *   A list suitable for short units\n *   e.g. \"3 ft, 7 in\"\n * * unit-narrow:\n *   A list suitable for narrow units, where space on the screen is very limited.\n *   e.g. \"3\u2032 7\u2033\"\n *\n * See https://www.unicode.org/reports/tr35/tr35-49/tr35-general.html#ListPatterns for more details.\n *\n * @param {string[]} list The array of values to format into a list.\n * @param {Object} [param0]\n * @param {string} [param0.localeCode] The locale to use (e.g. en-US).\n * @param {\"standard\"|\"standard-short\"|\"or\"|\"or-short\"|\"unit\"|\"unit-short\"|\"unit-narrow\"} [param0.style=\"standard\"] The style to format the list with.\n * @returns {string} The formatted list.\n */\nexport function formatList(list, { localeCode = \"\", style = \"standard\" } = {}) {\n    const locale = localeCode || user.lang || \"en-US\";\n    const formatter = new Intl.ListFormat(locale, LIST_STYLES[style]);\n    return formatter.format(list);\n}\n", "/**\n * Converts a locale from JavaScript to Python format.\n *\n * Most of the time the conversion is simply to replace - with _.\n * Example: fr-BE \u2192 fr_BE\n *\n * Exception: Serbian can be written in both Latin and Cyrillic scripts\n * interchangeably, therefore its locale includes a special modifier\n * to indicate which script to use.\n * Example: sr-Latn \u2192 sr@latin\n *\n * BCP 47 (JS):\n *  language[-extlang][-script][-region][-variant][-extension][-privateuse]\n *  https://www.ietf.org/rfc/rfc5646.txt\n * XPG syntax (Python):\n *  language[_territory][.codeset][@modifier]\n *  https://www.gnu.org/software/libc/manual/html_node/Locale-Names.html\n *\n * @param {string} locale The locale formatted for use on the JavaScript-side.\n * @returns {string} The locale formatted for use on the Python-side.\n */\nexport function jsToPyLocale(locale) {\n    if (!locale) {\n        return \"\";\n    }\n    try {\n        var { language, script, region } = new Intl.Locale(locale);\n    } catch {\n        return locale;\n    }\n    let xpgLocale = language;\n    if (region) {\n        xpgLocale += `_${region}`;\n    }\n    switch (script) {\n        case \"Cyrl\":\n            xpgLocale += \"@Cyrl\";\n            break;\n        case \"Latn\":\n            xpgLocale += \"@latin\";\n            break;\n    }\n    return xpgLocale;\n}\n\n/**\n * Converts a locale from Python to JavaScript format.\n *\n * Most of the time the conversion is simply to replace _ with -.\n * Example: fr_BE \u2192 fr-BE\n *\n * Exception: Serbian can be written in both Latin and Cyrillic scripts\n * interchangeably, therefore its locale includes a special modifier\n * to indicate which script to use.\n * Example: sr@latin \u2192 sr-Latn\n *\n * BCP 47 (JS):\n *  language[-extlang][-script][-region][-variant][-extension][-privateuse]\n *  https://www.ietf.org/rfc/rfc5646.txt\n * XPG syntax (Python):\n *  language[_territory][.codeset][@modifier]\n *  https://www.gnu.org/software/libc/manual/html_node/Locale-Names.html\n *\n * @param {string} locale The locale formatted for use on the Python-side.\n * @returns {string} The locale formatted for use on the JavaScript-side.\n */\nexport function pyToJsLocale(locale) {\n    if (!locale) {\n        return \"\";\n    }\n    const regex = /^([a-z]+)(_[A-Z\\d]+)?(@.+)?$/;\n    const match = locale.match(regex);\n    if (!match) {\n        return locale;\n    }\n    const [, language, territory, modifier] = match;\n    const subtags = [language];\n    switch (modifier) {\n        case \"@Cyrl\":\n            subtags.push(\"Cyrl\");\n            break;\n        case \"@latin\":\n            subtags.push(\"Latn\");\n            break;\n    }\n    if (territory) {\n        subtags.push(territory.slice(1));\n    }\n    return subtags.join(\"-\");\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { isVisible } from \"@web/core/utils/ui\";\nimport { Mutex } from \"@web/core/utils/concurrency\";\n\n/**\n * @typedef MacroStep\n * @property {string} [trigger]\n * - An action returning a \"truthy\" value means that the step isn't successful.\n * - Current step index won't be incremented.\n * @property {string | (el: Element, step: MacroStep) => undefined | string} [action]\n * @property {*} [*] - any payload to the step.\n *\n * @typedef MacroDescriptor\n * @property {() => Element | undefined} trigger\n * @property {() => {}} action\n */\n\nexport const ACTION_HELPERS = {\n    click(el, _step) {\n        el.dispatchEvent(new MouseEvent(\"mouseover\"));\n        el.dispatchEvent(new MouseEvent(\"mouseenter\"));\n        el.dispatchEvent(new MouseEvent(\"mousedown\"));\n        el.dispatchEvent(new MouseEvent(\"mouseup\"));\n        el.click();\n        el.dispatchEvent(new MouseEvent(\"mouseout\"));\n        el.dispatchEvent(new MouseEvent(\"mouseleave\"));\n    },\n    text(el, step) {\n        // simulate an input (probably need to add keydown/keyup events)\n        this.click(el, step);\n        el.value = step.value;\n        el.dispatchEvent(new InputEvent(\"input\", { bubbles: true }));\n        el.dispatchEvent(new InputEvent(\"change\", { bubbles: true }));\n    },\n};\n\nconst mutex = new Mutex();\n\nclass TimeoutError extends Error {}\n\nclass Macro {\n    constructor(descr) {\n        this.name = descr.name || \"anonymous\";\n        this.timeoutDuration = descr.timeout || 0;\n        this.timeout = null;\n        this.currentIndex = 0;\n        this.checkDelay = descr.checkDelay || 0;\n        this.isComplete = false;\n        this.steps = descr.steps;\n        this.onStep = descr.onStep || (() => {});\n        this.onError = descr.onError;\n        this.onTimeout = descr.onTimeout;\n        this.setTimer();\n    }\n\n    async advance() {\n        if (this.isComplete) {\n            return;\n        }\n        const step = this.steps[this.currentIndex];\n        const [proceedToAction, el] = this.checkTrigger(step);\n        if (proceedToAction) {\n            this.safeCall(this.onStep, el, step);\n            const actionResult = await this.performAction(el, step);\n            if (!actionResult) {\n                // If falsy action result, it means the action worked properly.\n                // So we can proceed to the next step.\n                this.currentIndex++;\n                if (this.currentIndex === this.steps.length) {\n                    this.isComplete = true;\n                    browser.clearTimeout(this.timeout);\n                } else {\n                    this.setTimer();\n                    await this.advance();\n                }\n            }\n        }\n    }\n\n    /**\n     * Find the trigger and assess whether it can continue on performing the actions.\n     * @param {{ trigger: string | () => Element | null }} param0\n     * @returns {[proceedToAction: boolean; el: Element | undefined]}\n     */\n    checkTrigger({ trigger }) {\n        let el;\n\n        if (!trigger) {\n            return [true, el];\n        }\n\n        if (typeof trigger === \"function\") {\n            el = this.safeCall(trigger);\n        } else if (typeof trigger === \"string\") {\n            const triggerEl = document.querySelector(trigger);\n            el = isVisible(triggerEl) && triggerEl;\n        } else {\n            throw new Error(`Trigger can only be string or function.`);\n        }\n\n        if (el) {\n            return [true, el];\n        } else {\n            return [false, el];\n        }\n    }\n\n    /**\n     * Calls the `step.action` expecting no return to be successful.\n     * @param {Element} el\n     * @param {Step} step\n     */\n    async performAction(el, step) {\n        const action = step.action;\n        let actionResult;\n        if (action in ACTION_HELPERS) {\n            actionResult = ACTION_HELPERS[action](el, step);\n        } else if (typeof action === \"function\") {\n            actionResult = await this.safeCall(action, el, step);\n        }\n        return actionResult;\n    }\n\n    safeCall(fn, ...args) {\n        if (this.isComplete) {\n            return;\n        }\n        try {\n            return fn(...args);\n        } catch (e) {\n            this.handleError(e);\n        }\n    }\n\n    setTimer() {\n        if (this.timeoutDuration) {\n            browser.clearTimeout(this.timeout);\n            this.timeout = browser.setTimeout(() => {\n                if (this.onTimeout) {\n                    const index = this.currentIndex;\n                    const step = this.steps[index];\n                    this.safeCall(this.onTimeout, step, index);\n                } else {\n                    const error = new TimeoutError(\"Step timeout\");\n                    this.handleError(error);\n                }\n            }, this.timeoutDuration);\n        }\n    }\n\n    handleError(error) {\n        // mark the macro as complete, so it can be cleaned up from the\n        // engine\n        this.isComplete = true;\n        browser.clearTimeout(this.timeout);\n        if (this.onError) {\n            const index = this.currentIndex;\n            const step = this.steps[index];\n            this.onError(error, step, index);\n        } else {\n            console.error(error);\n        }\n    }\n}\n\nexport class MacroEngine {\n    constructor(params = {}) {\n        this.isRunning = false;\n        this.timeout = null;\n        this.target = params.target || document.body;\n        this.defaultCheckDelay = params.defaultCheckDelay ?? 750;\n        this.macros = new Set();\n        this.macroMutationObserver = new MacroMutationObserver(() => this.delayedCheck());\n    }\n\n    async activate(descr, exclusive = false) {\n        if (this.exclusive) {\n            return;\n        }\n        this.exclusive = exclusive;\n        // micro task tick to make sure we add the macro in a new call stack,\n        // so we are guaranteed that we are not iterating on the current macros\n        await Promise.resolve();\n        const macro = new Macro(descr);\n        if (exclusive) {\n            this.macros = new Set([macro]);\n        } else {\n            this.macros.add(macro);\n        }\n        this.start();\n    }\n\n    start() {\n        if (!this.isRunning) {\n            this.isRunning = true;\n            this.macroMutationObserver.observe(this.target);\n        }\n        this.delayedCheck();\n    }\n\n    stop() {\n        if (this.isRunning) {\n            this.isRunning = false;\n            browser.clearTimeout(this.timeout);\n            this.timeout = null;\n            this.macroMutationObserver.disconnect();\n        }\n    }\n\n    delayedCheck() {\n        if (this.timeout) {\n            browser.clearTimeout(this.timeout);\n        }\n        this.timeout = browser.setTimeout(\n            () => mutex.exec(this.advanceMacros.bind(this)),\n            this.getCheckDelay() || this.defaultCheckDelay\n        );\n    }\n\n    getCheckDelay() {\n        // If a macro has a checkDelay different from 0, use it. Select the minimum.\n        // For example knowledge has a macro with a delay of 10ms. We don't want to wait\n        // longer because of other running tours.\n        return [...this.macros]\n            .map((m) => m.checkDelay)\n            .filter((delay) => delay > 0)\n            .reduce((m, v) => Math.min(m, v), this.defaultCheckDelay);\n    }\n\n    async advanceMacros() {\n        await Promise.all([...this.macros].map((macro) => macro.advance()));\n        for (const macro of this.macros) {\n            if (macro.isComplete) {\n                this.macros.delete(macro);\n            }\n        }\n        if (this.macros.size === 0) {\n            this.stop();\n        }\n    }\n}\n\nexport class MacroMutationObserver {\n    observerOptions = {\n        attributes: true,\n        childList: true,\n        subtree: true,\n        characterData: true,\n    };\n    constructor(callback) {\n        this.callback = callback;\n        this.observer = new MutationObserver((mutationList, observer) => {\n            callback();\n            mutationList.forEach((mutationRecord) =>\n                Array.from(mutationRecord.addedNodes).forEach((node) => {\n                    let iframes = [];\n                    if (String(node.tagName).toLowerCase() === \"iframe\") {\n                        iframes = [node];\n                    } else if (node instanceof HTMLElement) {\n                        iframes = Array.from(node.querySelectorAll(\"iframe\"));\n                    }\n                    iframes.forEach((iframeEl) =>\n                        this.observeIframe(iframeEl, observer, () => callback())\n                    );\n                    this.findAllShadowRoots(node).forEach((shadowRoot) =>\n                        observer.observe(shadowRoot, this.observerOptions)\n                    );\n                })\n            );\n        });\n    }\n    disconnect() {\n        this.observer.disconnect();\n    }\n    findAllShadowRoots(node, shadowRoots = []) {\n        if (node.shadowRoot) {\n            shadowRoots.push(node.shadowRoot);\n            this.findAllShadowRoots(node.shadowRoot, shadowRoots);\n        }\n        node.childNodes.forEach((child) => {\n            this.findAllShadowRoots(child, shadowRoots);\n        });\n        return shadowRoots;\n    }\n    observe(target) {\n        this.observer.observe(target, this.observerOptions);\n        //When iframes already exist at \"this.target\" initialization\n        target\n            .querySelectorAll(\"iframe\")\n            .forEach((el) => this.observeIframe(el, this.observer, () => this.callback()));\n        //When shadowDom already exist at \"this.target\" initialization\n        this.findAllShadowRoots(target).forEach((shadowRoot) => {\n            this.observer.observe(shadowRoot, this.observerOptions);\n        });\n    }\n    observeIframe(iframeEl, observer, callback) {\n        const observerOptions = {\n            attributes: true,\n            childList: true,\n            subtree: true,\n            characterData: true,\n        };\n        const observeIframeContent = () => {\n            if (iframeEl.contentDocument) {\n                iframeEl.contentDocument.addEventListener(\"load\", (event) => {\n                    callback();\n                    observer.observe(event.target, observerOptions);\n                });\n                if (!iframeEl.src || iframeEl.contentDocument.readyState === \"complete\") {\n                    callback();\n                    observer.observe(iframeEl.contentDocument, observerOptions);\n                }\n            }\n        };\n        observeIframeContent();\n        iframeEl.addEventListener(\"load\", observeIframeContent);\n    }\n}\n", "import { Component, xml } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useRegistry } from \"@web/core/registry_hook\";\nimport { ErrorHandler } from \"@web/core/utils/components\";\n\nconst mainComponents = registry.category(\"main_components\");\n\nmainComponents.addValidation({\n    Component: { validate: (c) => c.prototype instanceof Component },\n    props: { type: Object, optional: true }\n});\n\nexport class MainComponentsContainer extends Component {\n    static components = { ErrorHandler };\n    static props = {};\n    static template = xml`\n    <div class=\"o-main-components-container\">\n        <t t-foreach=\"Components.entries\" t-as=\"C\" t-key=\"C[0]\">\n            <ErrorHandler onError=\"error => this.handleComponentError(error, C)\">\n                <t t-component=\"C[1].Component\" t-props=\"C[1].props\"/>\n            </ErrorHandler>\n        </t>\n    </div>\n    `;\n\n    setup() {\n        this.Components = useRegistry(mainComponents);\n    }\n\n    handleComponentError(error, C) {\n        // remove the faulty component and rerender without it\n        this.Components.entries.splice(this.Components.entries.indexOf(C), 1);\n        this.render();\n        /**\n         * we rethrow the error to notify the user something bad happened.\n         * We do it after a tick to make sure owl can properly finish its\n         * rendering\n         */\n        Promise.resolve().then(() => {\n            throw error;\n        });\n    }\n}\n", "import { Component, onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { ModelFieldSelectorPopover } from \"./model_field_selector_popover\";\nimport { useLoadFieldInfo, useLoadPathDescription } from \"./utils\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\n\nexport class ModelFieldSelector extends Component {\n    static template = \"web._ModelFieldSelector\";\n    static components = {\n        Popover: ModelFieldSelectorPopover,\n    };\n    static props = {\n        resModel: String,\n        path: { optional: true },\n        allowEmpty: { type: Boolean, optional: true },\n        readonly: { type: Boolean, optional: true },\n        showSearchInput: { type: Boolean, optional: true },\n        isDebugMode: { type: Boolean, optional: true },\n        update: { type: Function, optional: true },\n        filter: { type: Function, optional: true },\n        followRelations: { type: Boolean, optional: true },\n        showDebugInput: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        readonly: true,\n        allowEmpty: false,\n        isDebugMode: false,\n        showSearchInput: true,\n        update: () => {},\n        followRelations: true,\n    };\n\n    setup() {\n        this.loadPathDescription = useLoadPathDescription();\n        const loadFieldInfo = useLoadFieldInfo();\n        this.popover = usePopover(this.constructor.components.Popover, {\n            popoverClass: \"o_popover_field_selector\",\n            onClose: async () => {\n                if (this.newPath !== null) {\n                    const fieldInfo = await loadFieldInfo(this.props.resModel, this.newPath);\n                    this.props.update(this.newPath, fieldInfo);\n                }\n            },\n        });\n        this.keepLast = new KeepLast();\n        this.state = useState({ isInvalid: false, displayNames: [] });\n        onWillStart(() => this.updateState(this.props));\n        onWillUpdateProps((nextProps) => this.updateState(nextProps));\n    }\n\n    openPopover(currentTarget) {\n        if (this.props.readonly) {\n            return;\n        }\n        this.newPath = null;\n        this.popover.open(currentTarget, {\n            resModel: this.props.resModel,\n            path: this.props.path,\n            update: (path, _fieldInfo, debug = false) => {\n                this.newPath = path;\n                if (!debug) {\n                    this.updateState({ ...this.props, path }, true);\n                }\n            },\n            showSearchInput: this.props.showSearchInput,\n            isDebugMode: this.props.isDebugMode,\n            filter: this.props.filter,\n            followRelations: this.props.followRelations,\n            showDebugInput: this.props.showDebugInput,\n        });\n    }\n\n    async updateState(params, isConcurrent) {\n        const { resModel, path, allowEmpty } = params;\n        let prom = this.loadPathDescription(resModel, path, allowEmpty);\n        if (isConcurrent) {\n            prom = this.keepLast.add(prom);\n        }\n        const state = await prom;\n        Object.assign(this.state, state);\n    }\n\n    clear() {\n        if (this.popover.isOpen) {\n            this.newPath = \"\";\n            this.popover.close();\n            return;\n        }\n        this.props.update(\"\", { resModel: this.props.resModel, fieldDef: null });\n    }\n}\n", "import { Component, onWillStart, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nclass Page {\n    constructor(resModel, fieldDefs, options = {}) {\n        this.resModel = resModel;\n        this.fieldDefs = fieldDefs;\n        const { previousPage = null, selectedName = null, isDebugMode } = options;\n        this.previousPage = previousPage;\n        this.selectedName = selectedName;\n        this.isDebugMode = isDebugMode;\n        this.sortedFieldNames = sortBy(Object.keys(fieldDefs), (key) => fieldDefs[key].string);\n        this.fieldNames = this.sortedFieldNames;\n        this.query = \"\";\n        this.focusedFieldName = null;\n        this.resetFocusedFieldName();\n    }\n\n    get path() {\n        const previousPath = this.previousPage?.path || \"\";\n        if (this.selectedName) {\n            if (previousPath) {\n                return `${previousPath}.${this.selectedName}`;\n            } else {\n                return this.selectedName;\n            }\n        }\n        return previousPath;\n    }\n\n    get selectedField() {\n        return this.fieldDefs[this.selectedName];\n    }\n\n    get title() {\n        const prefix = this.previousPage?.previousPage ? \"... > \" : \"\";\n        const title = this.previousPage?.selectedField.string || \"\";\n        if (prefix.length || title.length) {\n            return `${prefix}${title}`;\n        }\n        return _t(\"Select a field\");\n    }\n\n    focus(direction) {\n        if (!this.fieldNames.length) {\n            return;\n        }\n        const index = this.fieldNames.indexOf(this.focusedFieldName);\n        if (direction === \"previous\") {\n            if (index === 0) {\n                this.focusedFieldName = this.fieldNames[this.fieldNames.length - 1];\n            } else {\n                this.focusedFieldName = this.fieldNames[index - 1];\n            }\n        } else {\n            if (index === this.fieldNames.length - 1) {\n                this.focusedFieldName = this.fieldNames[0];\n            } else {\n                this.focusedFieldName = this.fieldNames[index + 1];\n            }\n        }\n    }\n\n    resetFocusedFieldName() {\n        if (this.selectedName && this.fieldNames.includes(this.selectedName)) {\n            this.focusedFieldName = this.selectedName;\n        } else {\n            this.focusedFieldName = this.fieldNames.length ? this.fieldNames[0] : null;\n        }\n    }\n\n    searchFields(query = \"\") {\n        this.query = query;\n        this.fieldNames = this.sortedFieldNames;\n        if (query) {\n            this.fieldNames = fuzzyLookup(query, this.fieldNames, (key) => {\n                const vals = [this.fieldDefs[key].string];\n                if (this.isDebugMode) {\n                    vals.push(key);\n                }\n                return vals;\n            });\n        }\n        this.resetFocusedFieldName();\n    }\n}\n\nexport class ModelFieldSelectorPopover extends Component {\n    static template = \"web.ModelFieldSelectorPopover\";\n    static props = {\n        close: Function,\n        filter: { type: Function, optional: true },\n        followRelations: { type: Boolean, optional: true },\n        showDebugInput: { type: Boolean, optional: true },\n        isDebugMode: { type: Boolean, optional: true },\n        path: { optional: true },\n        resModel: String,\n        showSearchInput: { type: Boolean, optional: true },\n        update: Function,\n    };\n    static defaultProps = {\n        filter: (fieldDef) => fieldDef.searchable,\n        isDebugMode: false,\n        followRelations: true,\n    };\n\n    setup() {\n        this.fieldService = useService(\"field\");\n        this.state = useState({ page: null });\n        this.keepLast = new KeepLast();\n        this.debouncedSearchFields = debounce(this.searchFields.bind(this), 250);\n\n        onWillStart(async () => {\n            this.state.page = await this.loadPages(this.props.resModel, this.props.path);\n        });\n\n        const rootRef = useRef(\"root\");\n        useEffect(() => {\n            const focusedElement = rootRef.el.querySelector(\n                \".o_model_field_selector_popover_item.active\"\n            );\n            if (focusedElement) {\n                // current page can be empty (e.g. after a search)\n                focusedElement.scrollIntoView({ block: \"center\" });\n            }\n        });\n        useEffect(\n            () => {\n                if (this.props.showSearchInput) {\n                    const searchInput = rootRef.el.querySelector(\n                        \".o_model_field_selector_popover_search .o_input\"\n                    );\n                    searchInput.focus();\n                }\n            },\n            () => [this.state.page]\n        );\n    }\n\n    get showDebugInput() {\n        return this.props.showDebugInput ?? this.props.isDebugMode;\n    }\n\n    filter(fieldDefs, path) {\n        const filteredKeys = Object.keys(fieldDefs).filter((k) =>\n            this.props.filter(fieldDefs[k], path)\n        );\n        return Object.fromEntries(filteredKeys.map((k) => [k, fieldDefs[k]]));\n    }\n\n    async followRelation(fieldDef) {\n        const { modelsInfo } = await this.keepLast.add(\n            this.fieldService.loadPath(this.state.page.resModel, `${fieldDef.name}.*`)\n        );\n        this.state.page.selectedName = fieldDef.name;\n        const { resModel, fieldDefs } = modelsInfo.at(-1);\n        this.openPage(\n            new Page(resModel, this.filter(fieldDefs, this.state.page.path), {\n                previousPage: this.state.page,\n                isDebugMode: this.props.isDebugMode,\n            })\n        );\n    }\n\n    goToPreviousPage() {\n        this.keepLast.add(Promise.resolve());\n        this.openPage(this.state.page.previousPage);\n    }\n\n    async loadNewPath(path) {\n        const newPage = await this.keepLast.add(this.loadPages(this.props.resModel, path));\n        this.openPage(newPage);\n    }\n\n    async loadPages(resModel, path) {\n        if (typeof path !== \"string\" || !path.length) {\n            const fieldDefs = await this.fieldService.loadFields(resModel);\n            return new Page(resModel, this.filter(fieldDefs, path), {\n                isDebugMode: this.props.isDebugMode,\n            });\n        }\n        const { isInvalid, modelsInfo, names } = await this.fieldService.loadPath(resModel, path);\n        switch (isInvalid) {\n            case \"model\":\n                throw new Error(`Invalid model name: ${resModel}`);\n            case \"path\": {\n                const { resModel, fieldDefs } = modelsInfo[0];\n                return new Page(resModel, this.filter(fieldDefs, path), {\n                    selectedName: path,\n                    isDebugMode: this.props.isDebugMode,\n                });\n            }\n            default: {\n                let page = null;\n                for (let index = 0; index < names.length; index++) {\n                    const name = names[index];\n                    const { resModel, fieldDefs } = modelsInfo[index];\n                    page = new Page(resModel, this.filter(fieldDefs, path), {\n                        previousPage: page,\n                        selectedName: name,\n                        isDebugMode: this.props.isDebugMode,\n                    });\n                }\n                return page;\n            }\n        }\n    }\n\n    openPage(page) {\n        this.state.page = page;\n        this.state.page.searchFields();\n        this.props.update(page.path);\n    }\n\n    searchFields(query) {\n        this.state.page.searchFields(query);\n    }\n\n    selectField(field) {\n        if (field.type === \"properties\") {\n            return this.followRelation(field);\n        }\n        this.keepLast.add(Promise.resolve());\n        this.state.page.selectedName = field.name;\n        this.props.update(this.state.page.path, field);\n        this.props.close();\n    }\n\n    onDebugInputKeydown(ev) {\n        switch (ev.key) {\n            case \"Enter\": {\n                ev.preventDefault();\n                ev.stopPropagation();\n                this.loadNewPath(ev.currentTarget.value);\n                break;\n            }\n        }\n    }\n\n    // @TODO should rework/improve this and maybe use hotkeys\n    async onInputKeydown(ev) {\n        const { page } = this.state;\n        switch (ev.key) {\n            case \"ArrowUp\": {\n                if (ev.target.selectionStart === 0) {\n                    page.focus(\"previous\");\n                }\n                break;\n            }\n            case \"ArrowDown\": {\n                if (ev.target.selectionStart === page.query.length) {\n                    page.focus(\"next\");\n                }\n                break;\n            }\n            case \"ArrowLeft\": {\n                if (ev.target.selectionStart === 0 && page.previousPage) {\n                    this.goToPreviousPage();\n                }\n                break;\n            }\n            case \"ArrowRight\": {\n                if (ev.target.selectionStart === page.query.length) {\n                    const focusedFieldName = this.state.page.focusedFieldName;\n                    if (focusedFieldName) {\n                        const fieldDef = this.state.page.fieldDefs[focusedFieldName];\n                        if (fieldDef.relation || fieldDef.type === \"properties\") {\n                            this.followRelation(fieldDef);\n                        }\n                    }\n                }\n                break;\n            }\n            case \"Enter\": {\n                const focusedFieldName = this.state.page.focusedFieldName;\n                if (focusedFieldName) {\n                    const fieldDef = this.state.page.fieldDefs[focusedFieldName];\n                    this.selectField(fieldDef);\n                } else {\n                    ev.preventDefault();\n                    ev.stopPropagation();\n                }\n                break;\n            }\n            case \"Escape\": {\n                ev.preventDefault();\n                ev.stopPropagation();\n                this.props.close();\n                break;\n            }\n        }\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\n\nfunction makeString(value) {\n    return String(value ?? \"-\");\n}\n\nexport function useLoadFieldInfo(fieldService) {\n    fieldService ||= useService(\"field\");\n    return async (resModel, path) => {\n        if (typeof path !== \"string\" || !path) {\n            return { resModel, fieldDef: null };\n        }\n        const { isInvalid, names, modelsInfo } = await fieldService.loadPath(resModel, path);\n        if (isInvalid) {\n            return { resModel, fieldDef: null };\n        }\n        const name = names.at(-1);\n        const modelInfo = modelsInfo.at(-1);\n        return { resModel: modelInfo.resModel, fieldDef: modelInfo.fieldDefs[name] };\n    };\n}\n\nexport function useLoadPathDescription(fieldService) {\n    fieldService ||= useService(\"field\");\n    return async (resModel, path, allowEmpty) => {\n        if ([0, 1].includes(path)) {\n            return { isInvalid: false, displayNames: [makeString(path)] };\n        }\n        if (allowEmpty && !path) {\n            return { isInvalid: false, displayNames: [] };\n        }\n        if (typeof path !== \"string\" || !path) {\n            return { isInvalid: true, displayNames: [makeString()] };\n        }\n        const { isInvalid, modelsInfo, names } = await fieldService.loadPath(resModel, path);\n        const result = { isInvalid: !!isInvalid, displayNames: [] };\n        if (!isInvalid) {\n            const lastName = names.at(-1);\n            const lastFieldDef = modelsInfo.at(-1).fieldDefs[lastName];\n            if ([\"properties\", \"properties_definition\"].includes(lastFieldDef.type)) {\n                // there is no known case where we want to select a 'properties' field directly\n                result.isInvalid = true;\n            }\n        }\n        for (let index = 0; index < names.length; index++) {\n            const name = names[index];\n            const fieldDef = modelsInfo[index]?.fieldDefs[name];\n            result.displayNames.push(fieldDef?.string || makeString(name));\n        }\n        return result;\n    };\n}\n", "import { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { Component, onWillStart } from \"@odoo/owl\";\n\nexport class ModelSelector extends Component {\n    static template = \"web.ModelSelector\";\n    static components = { AutoComplete };\n    static props = {\n        onModelSelected: Function,\n        id: { type: String, optional: true },\n        value: { type: String, optional: true },\n        // list of models technical name, if not set\n        // we will fetch all models we have access to\n        models: { type: Array, optional: true },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n\n        onWillStart(async () => {\n            if (!this.props.models) {\n                this.models = await this._fetchAvailableModels();\n            } else {\n                this.models = await this.orm.call(\"ir.model\", \"display_name_for\", [\n                    this.props.models,\n                ]);\n            }\n\n            this.models = this.models.map((record) => ({\n                label: record.display_name,\n                technical: record.model,\n                classList: {\n                    [`o_model_selector_${record.model}`]: 1,\n                },\n            }));\n        });\n    }\n\n    get sources() {\n        return [this.optionsSource];\n    }\n    get optionsSource() {\n        return {\n            placeholder: _t(\"Loading...\"),\n            options: this.loadOptionsSource.bind(this),\n        };\n    }\n\n    onSelect(option) {\n        this.props.onModelSelected({\n            label: option.label,\n            technical: option.technical,\n        });\n    }\n\n    filterModels(name) {\n        if (!name) {\n            const visibleModels = this.models.slice(0, 8);\n            if (this.models.length - visibleModels.length > 0) {\n                visibleModels.push({\n                    label: _t(\"Start typing...\"),\n                    unselectable: true,\n                    classList: \"o_m2o_start_typing\",\n                });\n            }\n            return visibleModels;\n        }\n        return fuzzyLookup(name, this.models, (model) => model.technical + model.label);\n    }\n\n    loadOptionsSource(request) {\n        const options = this.filterModels(request);\n\n        if (!options.length) {\n            options.push({\n                label: _t(\"No records\"),\n                classList: \"o_m2o_no_result\",\n                unselectable: true,\n            });\n        }\n        return options;\n    }\n\n    /**\n     * Fetch the list of the models that can be\n     * selected for the relational properties.\n     */\n    async _fetchAvailableModels() {\n        const result = await this.orm.call(\"ir.model\", \"get_available_models\");\n        return result || [];\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { unique, zip } from \"@web/core/utils/arrays\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\nexport const ERROR_INACCESSIBLE_OR_MISSING = Symbol(\"INACCESSIBLE OR MISSING RECORD ID\");\n\nfunction isId(val) {\n    return Number.isInteger(val) && val >= 1;\n}\n\n/**\n * @typedef {Record<string, (string|ERROR_INACCESSIBLE_OR_MISSING)>} DisplayNames\n */\n\nexport const nameService = {\n    dependencies: [\"orm\"],\n    async: [\"loadDisplayNames\"],\n    start(env, { orm }) {\n        let cache = {};\n        const batches = {};\n\n        function clearCache() {\n            cache = {};\n        }\n\n        env.bus.addEventListener(\"ACTION_MANAGER:UPDATE\", clearCache);\n\n        function getMapping(resModel) {\n            if (!cache[resModel]) {\n                cache[resModel] = {};\n            }\n            return cache[resModel];\n        }\n\n        /**\n         * @param {string} resModel valid resModel name\n         * @param {DisplayNames} displayNames\n         */\n        function addDisplayNames(resModel, displayNames) {\n            const mapping = getMapping(resModel);\n            for (const resId in displayNames) {\n                mapping[resId] = new Deferred();\n                mapping[resId].resolve(displayNames[resId]);\n            }\n        }\n\n        /**\n         * @param {string} resModel valid resModel name\n         * @param {number[]} resIds valid ids\n         * @returns {Promise<DisplayNames>}\n         */\n        async function loadDisplayNames(resModel, resIds) {\n            const mapping = getMapping(resModel);\n            const proms = [];\n            const resIdsToFetch = [];\n            for (const resId of unique(resIds)) {\n                if (!isId(resId)) {\n                    throw new Error(`Invalid ID: ${resId}`);\n                }\n                if (!(resId in mapping)) {\n                    mapping[resId] = new Deferred();\n                    resIdsToFetch.push(resId);\n                }\n                proms.push(mapping[resId]);\n            }\n            if (resIdsToFetch.length) {\n                if (batches[resModel]) {\n                    batches[resModel].push(...resIdsToFetch);\n                } else {\n                    batches[resModel] = resIdsToFetch;\n                    await Promise.resolve();\n                    const idsInBatch = unique(batches[resModel]);\n                    delete batches[resModel];\n\n                    const specification = { display_name: {} };\n                    orm.silent\n                        .webSearchRead(resModel, [[\"id\", \"in\", idsInBatch]], { specification })\n                        .then(({ records }) => {\n                            const displayNames = Object.fromEntries(\n                                records.map((rec) => [rec.id, rec.display_name])\n                            );\n                            for (const resId of idsInBatch) {\n                                mapping[resId].resolve(\n                                    resId in displayNames\n                                        ? displayNames[resId]\n                                        : ERROR_INACCESSIBLE_OR_MISSING\n                                );\n                            }\n                        });\n                }\n            }\n            const names = await Promise.all(proms);\n            return Object.fromEntries(zip(resIds, names));\n        }\n\n        return { addDisplayNames, clearCache, loadDisplayNames };\n    },\n};\n\nregistry.category(\"services\").add(\"name\", nameService);\n", "import { useEffect, useRef } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { scrollTo } from \"@web/core/utils/scrolling\";\nimport { debounce, throttleForAnimation } from \"@web/core/utils/timing\";\n\nconst ACTIVE_ELEMENT_CLASS = \"focus\";\nconst throttledElementFocus = throttleForAnimation((el) => el?.focus());\n\nfunction focusElement(el) {\n    throttledElementFocus.cancel();\n    throttledElementFocus(el);\n}\n\nclass NavigationItem {\n    constructor({ index, el, setActiveItem, options }) {\n        this.index = index;\n        this.options = options;\n        this.setActiveItem = setActiveItem;\n\n        this.el = el;\n        if (options.shouldFocusChildInput) {\n            const subInput = el.querySelector(\":scope input, :scope button, :scope textarea\");\n            this.target = subInput || el;\n        } else {\n            this.target = el;\n        }\n\n        const focus = () => this.focus(true);\n        const onMouseEnter = (ev) => this.onMouseEnter(ev);\n\n        this.target.addEventListener(\"focus\", focus);\n        this.target.addEventListener(\"mouseenter\", onMouseEnter);\n        this.removeListeners = () => {\n            this.target.removeEventListener(\"focus\", focus);\n            this.target.removeEventListener(\"mouseenter\", onMouseEnter);\n        };\n    }\n\n    select() {\n        this.focus();\n        this.target.click();\n    }\n\n    focus(skipRealFocus = false) {\n        scrollTo(this.target);\n        this.setActiveItem(this.index, this);\n        this.target.classList.add(ACTIVE_ELEMENT_CLASS);\n\n        if (!skipRealFocus && !this.options.virtualFocus) {\n            focusElement(this.target);\n        }\n    }\n\n    defocus() {\n        this.target.classList.remove(ACTIVE_ELEMENT_CLASS);\n    }\n\n    onMouseEnter() {\n        this.focus(true);\n        this.options.onMouseEnter?.(this);\n    }\n}\n\nclass Navigator {\n    /**\n     * @param {*} containerRef\n     * @param {NavigationOptions} options\n     */\n    constructor(containerRef, options, hotkeyService) {\n        this.enabled = false;\n        this.containerRef = containerRef;\n\n        const focusAt = (increment) => {\n            const isFocused = this.activeItem?.el.isConnected;\n            const index = this.currentActiveIndex + increment;\n            if (isFocused && index >= 0) {\n                return this.items[index % this.items.length]?.focus();\n            } else if (!isFocused && increment >= 0) {\n                return this.items[0]?.focus();\n            } else {\n                return this.items.at(-1)?.focus();\n            }\n        };\n\n        this.options = {\n            shouldFocusChildInput: true,\n            virtualFocus: false,\n            itemsSelector: \":scope .o-navigable\",\n            focusInitialElementOnDisabled: () => true,\n            ...options,\n\n            hotkeys: {\n                home: (index, items) => items[0]?.focus(),\n                end: (index, items) => items.at(-1)?.focus(),\n                tab: () => focusAt(+1),\n                \"shift+tab\": () => focusAt(-1),\n                arrowdown: () => focusAt(+1),\n                arrowup: () => focusAt(-1),\n                enter: (index, items) => {\n                    const item = items[index] || items[0];\n                    item?.select();\n                },\n                ...(options?.hotkeys || {}),\n            },\n        };\n\n        /**@type {Array<NavigationItem>} */\n        this.items = [];\n\n        /**@type {NavigationItem|undefined}*/\n        this.activeItem = undefined;\n        this.currentActiveIndex = -1;\n\n        this.initialFocusElement = undefined;\n        this.debouncedUpdate = debounce(() => this.update(), 100);\n\n        this.hotkeyRemoves = [];\n        this.hotkeyService = hotkeyService;\n\n        this.allowedInEditableHotkeys = [\"arrowup\", \"arrowdown\", \"enter\", \"tab\", \"shift+tab\"];\n    }\n\n    enable() {\n        if (!this.containerRef.el || this.targetObserver) {\n            return;\n        }\n\n        for (const [hotkey, callback] of Object.entries(this.options.hotkeys)) {\n            if (!callback) {\n                continue;\n            }\n\n            this.hotkeyRemoves.push(\n                this.hotkeyService.add(\n                    hotkey,\n                    () => callback(this.currentActiveIndex, this.items),\n                    {\n                        allowRepeat: true,\n                        bypassEditableProtection: this.allowedInEditableHotkeys.includes(hotkey),\n                    }\n                )\n            );\n        }\n\n        this.targetObserver = new MutationObserver(() => this.debouncedUpdate());\n        this.targetObserver.observe(this.containerRef.el, {\n            childList: true,\n            subtree: true,\n        });\n\n        this.initialFocusElement = document.activeElement;\n        this.currentActiveIndex = -1;\n        this.update();\n\n        if (this.options.onEnabled) {\n            this.options.onEnabled(this.items);\n        } else if (this.items.length > 0) {\n            this.items[0]?.focus();\n        }\n\n        this.enabled = true;\n    }\n\n    disable() {\n        if (!this.enabled) {\n            return;\n        }\n\n        if (this.targetObserver) {\n            this.targetObserver.disconnect();\n            this.targetObserver = undefined;\n        }\n\n        this.clearItems();\n        for (const removeHotkey of this.hotkeyRemoves) {\n            removeHotkey();\n        }\n        this.hotkeyRemoves = [];\n\n        if (this.options.focusInitialElementOnDisabled()) {\n            focusElement(this.initialFocusElement);\n        }\n\n        this.enabled = false;\n    }\n\n    update() {\n        if (!this.containerRef.el) {\n            return;\n        }\n        const oldItemsLength = this.items.length;\n        this.clearItems();\n\n        const elements = [...this.containerRef.el.querySelectorAll(this.options.itemsSelector)];\n        this.items = elements.map((el, index) => {\n            return new NavigationItem({\n                index,\n                el,\n                options: this.options,\n                setActiveItem: (index, el) => this.setActiveItem(index, el),\n            });\n        });\n\n        if (oldItemsLength != this.items.length && this.currentActiveIndex >= this.items.length) {\n            this.items.at(-1)?.focus();\n        }\n    }\n\n    setActiveItem(index, item) {\n        if (this.activeItem) {\n            this.activeItem.el.classList.remove(ACTIVE_ELEMENT_CLASS);\n        }\n        this.activeItem = item;\n        this.currentActiveIndex = index;\n    }\n\n    clearItems() {\n        for (const item of this.items) {\n            item.removeListeners();\n        }\n        this.items = [];\n    }\n}\n\n/**\n * @typedef {Object} NavigationOptions\n * @property {NavigationHotkeys} hotkeys\n * @property {Function} onEnabled\n * @property {Function} onMouseEnter\n * @property {Boolean} [virtualFocus=false] - If true, items are only visually\n * focused so the actual focus can be kept on another input.\n * @property {string} [itemsSelector=\":scope .o-navigable\"] - The selector used to get the list\n * of navigable elements.\n * @property {Function} focusInitialElementOnDisabled\n * @property {Boolean} [shouldFocusChildInput=false] - If true, elements like inputs or buttons\n * inside of the items are focused instead of the items themselves.\n */\n\n/**\n * @typedef {{\n *  home: keyHandlerCallback|undefined,\n *  end: keyHandlerCallback|undefined,\n *  tab: keyHandlerCallback|undefined,\n *  \"shift+tab\": keyHandlerCallback|undefined,\n *  arrowup: keyHandlerCallback|undefined,\n *  arrowdown: keyHandlerCallback|undefined,\n *  enter: keyHandlerCallback|undefined,\n *  arrowleft: keyHandlerCallback|undefined,\n *  arrowright: keyHandlerCallback|undefined,\n *  escape: keyHandlerCallback|undefined,\n *  space: keyHandlerCallback|undefined,\n * }} NavigationHotkeys\n */\n\n/**\n * Callback used to override the behaviour of a specific\n * key input.\n *\n * @callback keyHandlerCallback\n * @param {number} index                Current index.\n * @param {Array<NavigationItem>} items List of all navigation items.\n */\n\n/**\n * @typedef NavigationHook\n * @method enable\n * @method disable\n */\n\n/**\n * This hook adds keyboard navigation to items contained in an element.\n * It's purpose is to improve navigation in constrained context such\n * as dropdown and menus.\n *\n * This hook also has the following features:\n * - Hotkeys override and customization\n * - Navigation between inputs elements\n * - Optional virtual focus\n * - Focus on mouse enter\n *\n * @param {string|Object} containerRef\n * @param {NavigationOptions} options\n * @returns {NavigationHook}\n */\nexport function useNavigation(containerRef, options = {}) {\n    const hotkeyService = useService(\"hotkey\");\n    containerRef = typeof containerRef === \"string\" ? useRef(containerRef) : containerRef;\n    const navigator = new Navigator(containerRef, options, hotkeyService);\n\n    useEffect(\n        (container) => {\n            if (container) {\n                navigator.enable();\n            } else if (navigator) {\n                navigator.disable();\n            }\n        },\n        () => [containerRef.el]\n    );\n\n    return {\n        enable: () => navigator.enable(),\n        disable: () => navigator.disable(),\n    };\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { makeErrorFromResponse, ConnectionLostError } from \"@web/core/network/rpc\";\nimport { browser } from \"@web/core/browser/browser\";\n\n/* eslint-disable */\n/**\n * The following sections are from libraries, they have been slightly modified\n * to allow patching them during tests, but should not be linted, so that we can\n * keep a minimal diff that is easy to reapply when upgrading\n */\n// -----------------------------------------------------------------------------\n// Content Disposition Library\n// -----------------------------------------------------------------------------\n\n/*\n(The MIT License)\nCopyright (c) 2014-2017 Douglas Christopher Wilson\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/**\n * Stripped down to only parsing/decoding.\n * Slightly changed for export and lint compliance\n */\n\n/**\n * RegExp to match percent encoding escape.\n * @private\n */\nconst HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g;\n\n/**\n * RegExp to match non-latin1 characters.\n * @private\n */\nconst NON_LATIN1_REGEXP = /[^\\x20-\\x7e\\xa0-\\xff]/g;\n\n/**\n * RegExp to match quoted-pair in RFC 2616\n *\n * quoted-pair = \"\\\" CHAR\n * CHAR        = <any US-ASCII character (octets 0 - 127)>\n * @private\n */\nconst QESC_REGEXP = /\\\\([\\u0000-\\u007f])/g;\n\n/**\n * RegExp for various RFC 2616 grammar\n *\n * parameter     = token \"=\" ( token | quoted-string )\n * token         = 1*<any CHAR except CTLs or separators>\n * separators    = \"(\" | \")\" | \"<\" | \">\" | \"@\"\n *               | \",\" | \";\" | \":\" | \"\\\" | <\">\n *               | \"/\" | \"[\" | \"]\" | \"?\" | \"=\"\n *               | \"{\" | \"}\" | SP | HT\n * quoted-string = ( <\"> *(qdtext | quoted-pair ) <\"> )\n * qdtext        = <any TEXT except <\">>\n * quoted-pair   = \"\\\" CHAR\n * CHAR          = <any US-ASCII character (octets 0 - 127)>\n * TEXT          = <any OCTET except CTLs, but including LWS>\n * LWS           = [CRLF] 1*( SP | HT )\n * CRLF          = CR LF\n * CR            = <US-ASCII CR, carriage return (13)>\n * LF            = <US-ASCII LF, linefeed (10)>\n * SP            = <US-ASCII SP, space (32)>\n * HT            = <US-ASCII HT, horizontal-tab (9)>\n * CTL           = <any US-ASCII control character (octets 0 - 31) and DEL (127)>\n * OCTET         = <any 8-bit sequence of data>\n * @private\n */\nconst PARAM_REGEXP = /;[\\x09\\x20]*([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\\x09\\x20]*=[\\x09\\x20]*(\"(?:[\\x20!\\x23-\\x5b\\x5d-\\x7e\\x80-\\xff]|\\\\[\\x20-\\x7e])*\"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\\x09\\x20]*/g;\n\n/**\n * RegExp for various RFC 5987 grammar\n *\n * ext-value     = charset  \"'\" [ language ] \"'\" value-chars\n * charset       = \"UTF-8\" / \"ISO-8859-1\" / mime-charset\n * mime-charset  = 1*mime-charsetc\n * mime-charsetc = ALPHA / DIGIT\n *               / \"!\" / \"#\" / \"$\" / \"%\" / \"&\"\n *               / \"+\" / \"-\" / \"^\" / \"_\" / \"`\"\n *               / \"{\" / \"}\" / \"~\"\n * language      = ( 2*3ALPHA [ extlang ] )\n *               / 4ALPHA\n *               / 5*8ALPHA\n * extlang       = *3( \"-\" 3ALPHA )\n * value-chars   = *( pct-encoded / attr-char )\n * pct-encoded   = \"%\" HEXDIG HEXDIG\n * attr-char     = ALPHA / DIGIT\n *               / \"!\" / \"#\" / \"$\" / \"&\" / \"+\" / \"-\" / \".\"\n *               / \"^\" / \"_\" / \"`\" / \"|\" / \"~\"\n * @private\n */\nconst EXT_VALUE_REGEXP = /^([A-Za-z0-9!#$%&+\\-^_`{}~]+)'(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8}|)'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)$/;\n\n/**\n * RegExp for various RFC 6266 grammar\n *\n * disposition-type = \"inline\" | \"attachment\" | disp-ext-type\n * disp-ext-type    = token\n * disposition-parm = filename-parm | disp-ext-parm\n * filename-parm    = \"filename\" \"=\" value\n *                  | \"filename*\" \"=\" ext-value\n * disp-ext-parm    = token \"=\" value\n *                  | ext-token \"=\" ext-value\n * ext-token        = <the characters in token, followed by \"*\">\n * @private\n */\nconst DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\\x09\\x20]*(?:$|;)/;\n\n/**\n * Decode a RFC 6987 field value (gracefully).\n *\n * @param {string} str\n * @return {string}\n * @private\n */\nfunction decodefield(str) {\n    const match = EXT_VALUE_REGEXP.exec(str);\n\n    if (!match) {\n        throw new TypeError(\"invalid extended field value\");\n    }\n\n    const charset = match[1].toLowerCase();\n    const encoded = match[2];\n\n    switch (charset) {\n        case \"iso-8859-1\":\n            return encoded\n                .replace(HEX_ESCAPE_REPLACE_REGEXP, pdecode)\n                .replace(NON_LATIN1_REGEXP, \"?\");\n        case \"utf-8\":\n            return decodeURIComponent(encoded);\n        default:\n            throw new TypeError(\"unsupported charset in extended field\");\n    }\n}\n\n/**\n * Parse Content-Disposition header string.\n *\n * @param {string} string\n * @return {ContentDisposition}\n * @public\n */\nfunction parse(string) {\n    if (!string || typeof string !== \"string\") {\n        throw new TypeError(\"argument string is required\");\n    }\n\n    let match = DISPOSITION_TYPE_REGEXP.exec(string);\n\n    if (!match) {\n        throw new TypeError(\"invalid type format\");\n    }\n\n    // normalize type\n    let index = match[0].length;\n    const type = match[1].toLowerCase();\n\n    let key;\n    const names = [];\n    const params = {};\n    let value;\n\n    // calculate index to start at\n    index = PARAM_REGEXP.lastIndex = match[0].substr(-1) === \";\" ? index - 1 : index;\n\n    // match parameters\n    while ((match = PARAM_REGEXP.exec(string))) {\n        if (match.index !== index) {\n            throw new TypeError(\"invalid parameter format\");\n        }\n\n        index += match[0].length;\n        key = match[1].toLowerCase();\n        value = match[2];\n\n        if (names.indexOf(key) !== -1) {\n            throw new TypeError(\"invalid duplicate parameter\");\n        }\n\n        names.push(key);\n\n        if (key.indexOf(\"*\") + 1 === key.length) {\n            // decode extended value\n            key = key.slice(0, -1);\n            value = decodefield(value);\n\n            // overwrite existing value\n            params[key] = value;\n            continue;\n        }\n\n        if (typeof params[key] === \"string\") {\n            continue;\n        }\n\n        if (value[0] === '\"') {\n            // remove quotes and escapes\n            value = value.substr(1, value.length - 2).replace(QESC_REGEXP, \"$1\");\n        }\n\n        params[key] = value;\n    }\n\n    if (index !== -1 && index !== string.length) {\n        throw new TypeError(\"invalid parameter format\");\n    }\n\n    return new ContentDisposition(type, params);\n}\n\n/**\n * Percent decode a single character.\n *\n * @param {string} str\n * @param {string} hex\n * @return {string}\n * @private\n */\nfunction pdecode(str, hex) {\n    return String.fromCharCode(parseInt(hex, 16));\n}\n\n/**\n * Class for parsed Content-Disposition header for v8 optimization\n *\n * @public\n * @param {string} type\n * @param {object} parameters\n * @constructor\n */\nfunction ContentDisposition(type, parameters) {\n    this.type = type;\n    this.parameters = parameters;\n}\n\n// -----------------------------------------------------------------------------\n// download.js library\n// -----------------------------------------------------------------------------\n\n/*\nMIT License\nCopyright (c) 2016 dandavis\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n */\n\n/**\n * download.js v4.2, by dandavis; 2008-2018. [MIT] see http://danml.com/download.html for tests/usage\n * v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime\n * v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs\n * v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling.\n * v4 adds AMD/UMD, commonJS, and plain browser support\n * v4.1 adds url download capability via solo URL argument (same domain/CORS only)\n * v4.2 adds semantic variable names, long (over 2MB) dataURL support, and hidden by default temp anchors\n *\n * Slightly modified for export and lint compliance\n *\n * @param {Blob | File | String} data\n * @param {String} [filename]\n * @param {String} [mimetype]\n */\nfunction _download(data, filename, mimetype) {\n    let self = window, // this script is only for browsers anyway...\n        defaultMime = \"application/octet-stream\", // this default mime also triggers iframe downloads\n        mimeType = mimetype || defaultMime,\n        payload = data,\n        url = !filename && !mimetype && payload,\n        anchor = document.createElement(\"a\"),\n        toString = function (a) {\n            return String(a);\n        },\n        myBlob = self.Blob || self.MozBlob || self.WebKitBlob || toString,\n        fileName = filename || \"download\",\n        blob,\n        reader;\n    myBlob = myBlob.call ? myBlob.bind(self) : Blob;\n\n    if (String(this) === \"true\") {\n        //reverse arguments, allowing download.bind(true, \"text/xml\", \"export.xml\") to act as a callback\n        payload = [payload, mimeType];\n        mimeType = payload[0];\n        payload = payload[1];\n    }\n\n    if (url && url.length < 2048) {\n        // if no filename and no mime, assume a url was passed as the only argument\n        fileName = url.split(\"/\").pop().split(\"?\")[0];\n        anchor.href = url; // assign href prop to temp anchor\n        if (anchor.href.indexOf(url) !== -1) {\n            // if the browser determines that it's a potentially valid url path:\n            return new Promise((resolve, reject) => {\n                let xhr = new browser.XMLHttpRequest();\n                xhr.open(\"GET\", url, true);\n                configureBlobDownloadXHR(xhr, {\n                    onSuccess: resolve,\n                    onFailure: reject,\n                    url\n                });\n                xhr.send();\n            });\n        }\n    }\n\n    //go ahead and download dataURLs right away\n    if (/^data:[\\w+\\-]+\\/[\\w+\\-]+[,;]/.test(payload)) {\n        if (payload.length > 1024 * 1024 * 1.999 && myBlob !== toString) {\n            payload = dataUrlToBlob(payload);\n            mimeType = payload.type || defaultMime;\n        } else {\n            return navigator.msSaveBlob // IE10 can't do a[download], only Blobs:\n                ? navigator.msSaveBlob(dataUrlToBlob(payload), fileName)\n                : saver(payload); // everyone else can save dataURLs un-processed\n        }\n    }\n\n    blob = payload instanceof myBlob ? payload : new myBlob([payload], { type: mimeType });\n\n    function dataUrlToBlob(strUrl) {\n        let parts = strUrl.split(/[:;,]/),\n            type = parts[1],\n            decoder = parts[2] === \"base64\" ? atob : decodeURIComponent,\n            binData = decoder(parts.pop()),\n            mx = binData.length,\n            i = 0,\n            uiArr = new Uint8Array(mx);\n\n        for (i; i < mx; ++i) {\n            uiArr[i] = binData.charCodeAt(i);\n        }\n\n        return new myBlob([uiArr], { type });\n    }\n\n    function saver(url, winMode) {\n        if (\"download\" in anchor) {\n            //html5 A[download]\n            anchor.href = url;\n            anchor.setAttribute(\"download\", fileName);\n            anchor.className = \"download-js-link\";\n            anchor.innerText = _t(\"downloading...\");\n            anchor.style.display = \"none\";\n            document.body.appendChild(anchor);\n            setTimeout(() => {\n                anchor.click();\n                document.body.removeChild(anchor);\n                if (winMode === true) {\n                    setTimeout(() => {\n                        self.URL.revokeObjectURL(anchor.href);\n                    }, 250);\n                }\n            }, 66);\n            return true;\n        }\n\n        // handle non-a[download] safari as best we can:\n        if (/(Version)\\/(\\d+)\\.(\\d+)(?:\\.(\\d+))?.*Safari\\//.test(navigator.userAgent)) {\n            url = url.replace(/^data:([\\w\\/\\-+]+)/, defaultMime);\n            if (!window.open(url)) {\n                // popup blocked, offer direct download:\n                if (\n                    confirm(\n                        \"Displaying New Document\\n\\nUse Save As... to download, then click back to return to this page.\"\n                    )\n                ) {\n                    location.href = url;\n                }\n            }\n            return true;\n        }\n\n        //do iframe dataURL download (old ch+FF):\n        let f = document.createElement(\"iframe\");\n        document.body.appendChild(f);\n\n        if (!winMode) {\n            // force a mime that will download:\n            url = `data:${url.replace(/^data:([\\w\\/\\-+]+)/, defaultMime)}`;\n        }\n        f.src = url;\n        setTimeout(() => {\n            document.body.removeChild(f);\n        }, 333);\n    }\n\n    if (navigator.msSaveBlob) {\n        // IE10+ : (has Blob, but not a[download] or URL)\n        return navigator.msSaveBlob(blob, fileName);\n    }\n\n    if (self.URL) {\n        // simple fast and modern way using Blob and URL:\n        saver(self.URL.createObjectURL(blob), true);\n    } else {\n        // handle non-Blob()+non-URL browsers:\n        if (typeof blob === \"string\" || blob.constructor === toString) {\n            try {\n                return saver(`data:${mimeType};base64,${self.btoa(blob)}`);\n            } catch {\n                return saver(`data:${mimeType},${encodeURIComponent(blob)}`);\n            }\n        }\n\n        // Blob but not URL support:\n        reader = new FileReader();\n        reader.onload = function () {\n            saver(this.result);\n        };\n        reader.readAsDataURL(blob);\n    }\n    return true;\n}\n/* eslint-enable */\n\n// -----------------------------------------------------------------------------\n// Exported download functions\n// -----------------------------------------------------------------------------\n\n/**\n * Download data as a file\n *\n * @param {Object} data\n * @param {String} filename\n * @param {String} mimetype\n * @returns {Boolean}\n *\n * Note: the actual implementation is certainly unconventional, but sadly\n * necessary to be able to test code using the download function\n */\nexport function downloadFile(data, filename, mimetype) {\n    return downloadFile._download(data, filename, mimetype);\n}\ndownloadFile._download = _download;\n\n/**\n * Download a file from form or server url\n *\n * This function is meant to call a controller with some data\n * and download the response.\n *\n * Note: the actual implementation is certainly unconventional, but sadly\n * necessary to be able to test code using the download function\n *\n * @param {*} options\n * @returns {Promise<any>}\n */\nexport function download(options) {\n    return download._download(options);\n}\n\ndownload._download = (options) => {\n    return new Promise((resolve, reject) => {\n        const xhr = new browser.XMLHttpRequest();\n        let data;\n        if (Object.prototype.hasOwnProperty.call(options, \"form\")) {\n            xhr.open(options.form.method, options.form.action);\n            data = new FormData(options.form);\n        } else {\n            xhr.open(\"POST\", options.url);\n            data = new FormData();\n            Object.entries(options.data).forEach((entry) => {\n                const [key, value] = entry;\n                data.append(key, value);\n            });\n        }\n        data.append(\"token\", \"dummy-because-api-expects-one\");\n        if (odoo.csrf_token) {\n            data.append(\"csrf_token\", odoo.csrf_token);\n        }\n        configureBlobDownloadXHR(xhr, {\n            onSuccess: resolve,\n            onFailure: reject,\n            url: options.url,\n        });\n        xhr.send(data);\n    });\n};\n\n/**\n * Setup a download xhr request response handling\n * (onload, onerror, responseType), with hooks when the download succeeds or\n * fails.\n *\n * @param {XMLHttpRequest} xhr\n * @param {object} [options]\n * @param {(filename: string) => void} [options.onSuccess]\n * @param {(Error) => void} [options.onFailure]\n * @param {string} [options.url]\n */\nexport function configureBlobDownloadXHR(\n    xhr,\n    { onSuccess = () => {}, onFailure = () => {}, url } = {}\n) {\n    xhr.responseType = \"blob\";\n    xhr.onload = () => {\n        const mimetype = xhr.response.type;\n        const header = (xhr.getResponseHeader(\"Content-Disposition\") || \"\").replace(/;$/, \"\");\n        // replace because apparently we send some C-D headers with a trailing \";\"\n        const filename = header ? parse(header).parameters.filename : null;\n        // In Odoo, the default mimetype, including for JSON errors is text/html (ref: http.py:Root.get_response )\n        // in that case, in order to also be able to download html files, we check if we get a proper filename to be able to download\n        if (xhr.status === 200 && (mimetype !== \"text/html\" || filename)) {\n            _download(xhr.response, filename, mimetype);\n            onSuccess(filename);\n        } else if (xhr.status === 502) {\n            // If Odoo is behind another server (nginx)\n            onFailure(new ConnectionLostError(url));\n        } else {\n            const decoder = new FileReader();\n            decoder.onload = () => {\n                const contents = decoder.result;\n                const doc = new DOMParser().parseFromString(contents, \"text/html\");\n                const nodes =\n                    doc.body.children.length === 0 ? [doc.body] : doc.body.children;\n\n                let error;\n                try {\n                    // a Serialized python Error\n                    const node = nodes[1] || nodes[0];\n                    error = JSON.parse(node.textContent);\n                } catch {\n                    error = {\n                        message: \"Arbitrary Uncaught Python Exception\",\n                        data: {\n                            debug:\n                                `${xhr.status}` +\n                                `\\n` +\n                                `${nodes.length > 0 ? nodes[0].textContent : \"\"}\n                                ${nodes.length > 1 ? nodes[1].textContent : \"\"}`,\n                        },\n                    };\n                }\n                error = makeErrorFromResponse(error);\n                onFailure(error);\n            };\n            decoder.readAsText(xhr.response);\n        }\n    };\n    xhr.onerror = () => {\n        onFailure(new ConnectionLostError(url));\n    };\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"../registry\";\n\nfunction checkResponseStatus(response) {\n    if (response.status === 502) {\n        throw new Error(\"Failed to fetch\");\n    }\n}\n\nexport async function get(route, readMethod = \"json\") {\n    const response = await browser.fetch(route, { method: \"GET\" });\n    checkResponseStatus(response);\n    return response[readMethod]();\n}\n\nexport async function post(route, params = {}, readMethod = \"json\") {\n    let formData = params;\n    if (!(formData instanceof FormData)) {\n        formData = new FormData();\n        for (const key in params) {\n            const value = params[key];\n            if (Array.isArray(value) && value.length) {\n                for (const val of value) {\n                    formData.append(key, val);\n                }\n            } else {\n                formData.append(key, value);\n            }\n        }\n    }\n    const response = await browser.fetch(route, {\n        body: formData,\n        method: \"POST\",\n    });\n    checkResponseStatus(response);\n    return response[readMethod]();\n}\n\nexport const httpService = {\n    start() {\n        return { get, post };\n    },\n};\n\nregistry.category(\"services\").add(\"http\", httpService);\n", "import { EventBus } from \"@odoo/owl\";\nimport { browser } from \"../browser/browser\";\n\nexport const rpcBus = new EventBus();\n\n// -----------------------------------------------------------------------------\n// Errors\n// -----------------------------------------------------------------------------\nexport class RPCError extends Error {\n    constructor() {\n        super(...arguments);\n        this.name = \"RPC_ERROR\";\n        this.type = \"server\";\n        this.code = null;\n        this.data = null;\n        this.exceptionName = null;\n        this.subType = null;\n    }\n}\n\nexport class ConnectionLostError extends Error {\n    constructor(url, ...args) {\n        super(`Connection to \"${url}\" couldn't be established or was interrupted`, ...args);\n        this.url = url;\n    }\n}\n\nexport class ConnectionAbortedError extends Error {}\n\nexport function makeErrorFromResponse(reponse) {\n    // Odoo returns error like this, in a error field instead of properly\n    // using http error codes...\n    const { code, data: errorData, message, type: subType } = reponse;\n    const error = new RPCError();\n    error.exceptionName = errorData.name;\n    error.subType = subType;\n    error.data = errorData;\n    error.message = message;\n    error.code = code;\n    return error;\n}\n\n// -----------------------------------------------------------------------------\n// Main RPC method\n// -----------------------------------------------------------------------------\nlet rpcId = 0;\nexport function rpc(url, params = {}, settings = {}) {\n    return rpc._rpc(url, params, settings);\n}\n// such that it can be overriden in tests\nrpc._rpc = function (url, params, settings) {\n    const XHR = browser.XMLHttpRequest;\n    const data = {\n        id: rpcId++,\n        jsonrpc: \"2.0\",\n        method: \"call\",\n        params: params,\n    };\n    const request = settings.xhr || new XHR();\n    let rejectFn;\n    const promise = new Promise((resolve, reject) => {\n        rejectFn = reject;\n        rpcBus.trigger(\"RPC:REQUEST\", { data, url, settings });\n        // handle success\n        request.addEventListener(\"load\", () => {\n            if (request.status === 502) {\n                // If Odoo is behind another server (eg.: nginx)\n                const error = new ConnectionLostError(url);\n                rpcBus.trigger(\"RPC:RESPONSE\", { data, settings, error });\n                reject(error);\n                return;\n            }\n            let params;\n            try {\n                params = JSON.parse(request.response);\n            } catch {\n                // the response isn't json parsable, which probably means that the rpc request could\n                // not be handled by the server, e.g. PoolError('The Connection Pool Is Full')\n                const error = new ConnectionLostError(url);\n                rpcBus.trigger(\"RPC:RESPONSE\", { data, settings, error });\n                return reject(error);\n            }\n            const { error: responseError, result: responseResult } = params;\n            if (!params.error) {\n                rpcBus.trigger(\"RPC:RESPONSE\", { data, settings, result: params.result });\n                return resolve(responseResult);\n            }\n            const error = makeErrorFromResponse(responseError);\n            error.id = data.id;\n            error.model = data.params.model;\n            rpcBus.trigger(\"RPC:RESPONSE\", { data, settings, error });\n            reject(error);\n        });\n        // handle failure\n        request.addEventListener(\"error\", () => {\n            const error = new ConnectionLostError(url);\n            rpcBus.trigger(\"RPC:RESPONSE\", { data, settings, error });\n            reject(error);\n        });\n        // configure and send request\n        request.open(\"POST\", url);\n        const headers = settings.headers || {};\n        headers[\"Content-Type\"] = \"application/json\";\n        for (let [header, value] of Object.entries(headers)) {\n            request.setRequestHeader(header, value);\n        }\n        request.send(JSON.stringify(data));\n    });\n    /**\n     * @param {Boolean} rejectError Returns an error if true. Allows you to cancel\n     *                  ignored rpc's in order to unblock the ui and not display an error.\n     */\n    promise.abort = function (rejectError = true) {\n        if (request.abort) {\n            request.abort();\n        }\n        const error = new ConnectionAbortedError(\"XmlHttpRequestError abort\");\n        rpcBus.trigger(\"RPC:RESPONSE\", { data, settings, error });\n        if (rejectError) {\n            rejectFn(error);\n        }\n    };\n    return promise;\n};\n", "import { scrollTo } from \"@web/core/utils/scrolling\";\n\nimport {\n    Component,\n    onWillUpdateProps,\n    useEffect,\n    useExternalListener,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\n\n/**\n * A notebook component that will render only the current page and allow\n * switching between its pages.\n *\n * You can also set pages using a template component. Use an array with\n * the `pages` props to do such rendering.\n *\n * Pages can also specify their index in the notebook.\n *\n *      e.g.:\n *          PageTemplate.template = xml`\n                    <h1 t-esc=\"props.heading\" />\n                    <p t-esc=\"props.text\" />`;\n\n *      `pages` could be:\n *      [\n *          {\n *              Component: PageTemplate,\n *              id: 'unique_id' // optional: can be given as defaultPage props to the notebook\n *              index: 1 // optional: page position in the notebook\n *              name: 'some_name' // optional\n *              title: \"Some Title 1\", // title displayed on the tab pane\n *              props: {\n *                  heading: \"Page 1\",\n *                  text: \"Text Content 1\",\n *              },\n *          },\n *          {\n *              Component: PageTemplate,\n *              title: \"Some Title 2\",\n *              props: {\n *                  heading: \"Page 2\",\n *                  text: \"Text Content 2\",\n *              },\n *          },\n *      ]\n *\n * <Notebook pages=\"pages\">\n *    <t t-set-slot=\"Page Name 1\" title=\"Some Title\" isVisible=\"bool\">\n *      <div>Page Content 1</div>\n *    </t>\n *    <t t-set-slot=\"Page Name 2\" title=\"Some Title\" isVisible=\"bool\">\n *      <div>Page Content 2</div>\n *    </t>\n * </Notebook>\n *\n * @extends Component\n */\n\nexport class Notebook extends Component {\n    static template = \"web.Notebook\";\n    static defaultProps = {\n        className: \"\",\n        orientation: \"horizontal\",\n        onPageUpdate: () => {},\n    };\n    static props = {\n        slots: { type: Object, optional: true },\n        pages: { type: Object, optional: true },\n        class: { optional: true },\n        className: { type: String, optional: true },\n        anchors: { type: Object, optional: true },\n        defaultPage: { type: String, optional: true },\n        orientation: { type: String, optional: true },\n        icons: { type: Object, optional: true },\n        onPageUpdate: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.activePane = useRef(\"activePane\");\n        this.anchorTarget = null;\n        this.pages = this.computePages(this.props);\n        this.state = useState({ currentPage: null });\n        this.state.currentPage = this.computeActivePage(this.props.defaultPage, true);\n        useExternalListener(browser, \"click\", this.onAnchorClicked);\n        useEffect(\n            () => {\n                this.props.onPageUpdate(this.state.currentPage);\n                if (this.anchorTarget) {\n                    const matchingEl = this.activePane.el.querySelector(`#${this.anchorTarget}`);\n                    scrollTo(matchingEl, { isAnchor: true });\n                    this.anchorTarget = null;\n                }\n                this.activePane.el?.classList.add(\"show\");\n            },\n            () => [this.state.currentPage]\n        );\n        onWillUpdateProps((nextProps) => {\n            const activateDefault =\n                this.props.defaultPage !== nextProps.defaultPage || !this.defaultVisible;\n            this.pages = this.computePages(nextProps);\n            this.state.currentPage = this.computeActivePage(nextProps.defaultPage, activateDefault);\n        });\n    }\n\n    get navItems() {\n        return this.pages.filter((e) => e[1].isVisible);\n    }\n\n    get page() {\n        const page = this.pages.find((e) => e[0] === this.state.currentPage)[1];\n        return page.Component && page;\n    }\n\n    onAnchorClicked(ev) {\n        if (!this.props.anchors) {\n            return;\n        }\n        const href = ev.target.closest(\"a\")?.getAttribute(\"href\");\n        if (!href) {\n            return;\n        }\n        const id = href.substring(1);\n        if (this.props.anchors[id]) {\n            if (this.state.currentPage !== this.props.anchors[id].target) {\n                ev.preventDefault();\n                this.anchorTarget = id;\n                this.state.currentPage = this.props.anchors[id].target;\n            }\n        }\n    }\n\n    activatePage(pageIndex) {\n        if (!this.disabledPages.includes(pageIndex) && this.state.currentPage !== pageIndex) {\n            this.activePane.el?.classList.remove(\"show\");\n            this.state.currentPage = pageIndex;\n        }\n    }\n\n    computePages(props) {\n        if (!props.slots && !props.pages) {\n            return [];\n        }\n        if (props.pages) {\n            for (const page of props.pages) {\n                page.isVisible = true;\n            }\n        }\n        this.disabledPages = [];\n        const pages = [];\n        const pagesWithIndex = [];\n        for (const [k, v] of Object.entries({ ...props.slots, ...props.pages })) {\n            const id = v.id || k;\n            if (v.index) {\n                pagesWithIndex.push([id, v]);\n            } else {\n                pages.push([id, v]);\n            }\n            if (v.isDisabled) {\n                this.disabledPages.push(k);\n            }\n        }\n        for (const page of pagesWithIndex) {\n            pages.splice(page[1].index, 0, page);\n        }\n        return pages;\n    }\n\n    computeActivePage(defaultPage, activateDefault) {\n        if (!this.pages.length) {\n            return null;\n        }\n        const pages = this.pages.filter((e) => e[1].isVisible).map((e) => e[0]);\n\n        if (defaultPage) {\n            if (!pages.includes(defaultPage)) {\n                this.defaultVisible = false;\n            } else {\n                this.defaultVisible = true;\n                if (activateDefault) {\n                    return defaultPage;\n                }\n            }\n        }\n        const current = this.state.currentPage;\n        if (!current || (current && !pages.includes(current))) {\n            return pages[0];\n        }\n\n        return current;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class Notification extends Component {\n    static template = \"web.NotificationWowl\";\n    static props = {\n        message: {\n            validate: (m) => {\n                return (\n                    typeof m === \"string\" ||\n                    (typeof m === \"object\" && typeof m.toString === \"function\")\n                );\n            },\n        },\n        title: { type: [String, Boolean, { toString: Function }], optional: true },\n        type: {\n            type: String,\n            optional: true,\n            validate: (t) => [\"warning\", \"danger\", \"success\", \"info\"].includes(t),\n        },\n        className: { type: String, optional: true },\n        buttons: {\n            type: Array,\n            element: {\n                type: Object,\n                shape: {\n                    name: { type: String },\n                    icon: { type: String, optional: true },\n                    primary: { type: Boolean, optional: true },\n                    onClick: Function,\n                },\n            },\n            optional: true,\n        },\n        close: { type: Function },\n        refresh: { type: Function },\n        freeze: { type: Function },\n    };\n    static defaultProps = {\n        buttons: [],\n        className: \"\",\n        type: \"warning\",\n    };\n}\n", "import { Notification } from \"./notification\";\nimport { Transition } from \"@web/core/transition\";\n\nimport { Component, xml, useState } from \"@odoo/owl\";\n\nexport class NotificationContainer extends Component {\n    static props = {\n        notifications: Object,\n    };\n\n    static template = xml`\n        <div class=\"o_notification_manager\">\n            <t t-foreach=\"notifications\" t-as=\"notification\" t-key=\"notification\">\n                <Transition leaveDuration=\"0\" immediate=\"true\" name=\"'o_notification_fade'\" t-slot-scope=\"transition\">\n                    <Notification t-props=\"notification_value.props\" className=\"(notification_value.props.className || '') + ' ' + transition.className\"/>\n                </Transition>\n            </t>\n        </div>`;\n    static components = { Notification, Transition };\n\n    setup() {\n        this.notifications = useState(this.props.notifications);\n    }\n}\n", "import { browser } from \"../browser/browser\";\nimport { registry } from \"../registry\";\nimport { NotificationContainer } from \"./notification_container\";\n\nimport { reactive } from \"@odoo/owl\";\n\nconst AUTOCLOSE_DELAY = 4000;\n\n/**\n * @typedef {Object} NotificationButton\n * @property {string} name\n * @property {string} [icon]\n * @property {boolean} [primary=false]\n * @property {function(): void} onClick\n *\n * @typedef {Object} NotificationOptions\n * @property {string} [title]\n * @property {number} [autocloseDelay=4000]\n * @property {\"warning\" | \"danger\" | \"success\" | \"info\"} [type]\n * @property {boolean} [sticky=false]\n * @property {string} [className]\n * @property {function(): void} [onClose]\n * @property {NotificationButton[]} [buttons]\n */\n\nexport const notificationService = {\n    notificationContainer: NotificationContainer,\n\n    start() {\n        let notifId = 0;\n        const notifications = reactive({});\n\n        registry.category(\"main_components\").add(\n            this.notificationContainer.name,\n            {\n                Component: this.notificationContainer,\n                props: { notifications },\n            },\n            { sequence: 100 }\n        );\n\n        /**\n         * @param {string} message\n         * @param {NotificationOptions} [options]\n         */\n        function add(message, options = {}) {\n            const id = ++notifId;\n            const closeFn = () => close(id);\n            const props = Object.assign({}, options, { message, close: closeFn });\n            const autocloseDelay = options.autocloseDelay ?? AUTOCLOSE_DELAY;\n            const sticky = props.sticky;\n            delete props.sticky;\n            delete props.onClose;\n            delete props.autocloseDelay;\n            let closeTimeout;\n            const refresh = sticky\n                ? () => {}\n                : () => {\n                      closeTimeout = browser.setTimeout(closeFn, autocloseDelay);\n                  };\n            const freeze = sticky\n                ? () => {}\n                : () => {\n                      browser.clearTimeout(closeTimeout);\n                  };\n            props.refresh = refreshAll;\n            props.freeze = freezeAll;\n            const notification = {\n                id,\n                props,\n                onClose: options.onClose,\n                refresh,\n                freeze,\n            };\n            notifications[id] = notification;\n            if (!sticky) {\n                closeTimeout = browser.setTimeout(closeFn, autocloseDelay);\n            }\n            return closeFn;\n        }\n\n        function refreshAll() {\n            for (const id in notifications) {\n                notifications[id].refresh();\n            }\n        }\n\n        function freezeAll() {\n            for (const id in notifications) {\n                notifications[id].freeze();\n            }\n        }\n\n        function close(id) {\n            if (notifications[id]) {\n                const notification = notifications[id];\n                if (notification.onClose) {\n                    notification.onClose();\n                }\n                delete notifications[id];\n            }\n        }\n\n        return { add };\n    },\n};\n\nregistry.category(\"services\").add(\"notification\", notificationService);\n", "import { registry } from \"@web/core/registry\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\n\n/**\n * This ORM service is the standard way to interact with the ORM in python from\n * the javascript codebase.\n */\n\n// -----------------------------------------------------------------------------\n// ORM\n// -----------------------------------------------------------------------------\n\n/**\n * One2many and Many2many fields expect a special command to manipulate the\n * relation they implement.\n *\n * Internally, each command is a 3-elements tuple where the first element is a\n * mandatory integer that identifies the command, the second element is either\n * the related record id to apply the command on (commands update, delete,\n * unlink and link) either 0 (commands create, clear and set), the third\n * element is either the ``values`` to write on the record (commands create\n * and update) either the new ``ids`` list of related records (command set),\n * either 0 (commands delete, unlink, link, and clear).\n */\nexport const x2ManyCommands = {\n    // (0, virtualID | false, { values })\n    CREATE: 0,\n    create(virtualID, values) {\n        delete values.id;\n        return [x2ManyCommands.CREATE, virtualID || false, values];\n    },\n    // (1, id, { values })\n    UPDATE: 1,\n    update(id, values) {\n        delete values.id;\n        return [x2ManyCommands.UPDATE, id, values];\n    },\n    // (2, id[, _])\n    DELETE: 2,\n    delete(id) {\n        return [x2ManyCommands.DELETE, id, false];\n    },\n    // (3, id[, _]) removes relation, but not linked record itself\n    UNLINK: 3,\n    unlink(id) {\n        return [x2ManyCommands.UNLINK, id, false];\n    },\n    // (4, id[, _])\n    LINK: 4,\n    link(id) {\n        return [x2ManyCommands.LINK, id, false];\n    },\n    // (5[, _[, _]])\n    CLEAR: 5,\n    clear() {\n        return [x2ManyCommands.CLEAR, false, false];\n    },\n    // (6, _, ids) replaces all linked records with provided ids\n    SET: 6,\n    set(ids) {\n        return [x2ManyCommands.SET, false, ids];\n    },\n};\n\nfunction validateModel(value) {\n    if (typeof value !== \"string\" || value.length === 0) {\n        throw new Error(`Invalid model name: ${value}`);\n    }\n}\nfunction validatePrimitiveList(name, type, value) {\n    if (!Array.isArray(value) || value.some((val) => typeof val !== type)) {\n        throw new Error(`Invalid ${name} list: ${value}`);\n    }\n}\nfunction validateObject(name, obj) {\n    if (typeof obj !== \"object\" || obj === null || Array.isArray(obj)) {\n        throw new Error(`${name} should be an object`);\n    }\n}\nfunction validateArray(name, array) {\n    if (!Array.isArray(array)) {\n        throw new Error(`${name} should be an array`);\n    }\n}\n\nexport const UPDATE_METHODS = [\n    \"unlink\",\n    \"create\",\n    \"write\",\n    \"web_save\",\n    \"action_archive\",\n    \"action_unarchive\",\n];\n\nexport class ORM {\n    constructor() {\n        this.rpc = rpc; // to be overridable by the SampleORM\n        /** @protected */\n        this._silent = false;\n    }\n\n    /** @returns {ORM} */\n    get silent() {\n        return Object.assign(Object.create(this), { _silent: true });\n    }\n\n    /**\n     * @param {string} model\n     * @param {string} method\n     * @param {any[]} [args=[]]\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any>}\n     */\n    call(model, method, args = [], kwargs = {}) {\n        validateModel(model);\n        const url = `/web/dataset/call_kw/${model}/${method}`;\n        const fullContext = Object.assign({}, user.context, kwargs.context || {});\n        const fullKwargs = Object.assign({}, kwargs, { context: fullContext });\n        const params = {\n            model,\n            method,\n            args,\n            kwargs: fullKwargs,\n        };\n        return this.rpc(url, params, { silent: this._silent });\n    }\n\n    /**\n     * @param {string} model\n     * @param {any[]} records\n     * @param {any} [kwargs=[]]\n     * @returns {Promise<number>}\n     */\n    create(model, records, kwargs = {}) {\n        validateArray(\"records\", records);\n        for (const record of records) {\n            validateObject(\"record\", record);\n        }\n        return this.call(model, \"create\", [records], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {number[]} ids\n     * @param {string[]} fields\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any[]>}\n     */\n    read(model, ids, fields, kwargs = {}) {\n        validatePrimitiveList(\"ids\", \"number\", ids);\n        if (fields) {\n            validatePrimitiveList(\"fields\", \"string\", fields);\n        }\n        if (!ids.length) {\n            return Promise.resolve([]);\n        }\n        return this.call(model, \"read\", [ids, fields], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {import(\"@web/core/domain\").DomainListRepr} domain\n     * @param {string[]} fields\n     * @param {string[]} groupby\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any[]>}\n     */\n    readGroup(model, domain, fields, groupby, kwargs = {}) {\n        validateArray(\"domain\", domain);\n        validatePrimitiveList(\"fields\", \"string\", fields);\n        validatePrimitiveList(\"groupby\", \"string\", groupby);\n        groupby = [...new Set(groupby)];\n        return this.call(model, \"read_group\", [], { ...kwargs, domain, fields, groupby });\n    }\n\n    /**\n     * @param {string} model\n     * @param {import(\"@web/core/domain\").DomainListRepr} domain\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any[]>}\n     */\n    search(model, domain, kwargs = {}) {\n        validateArray(\"domain\", domain);\n        return this.call(model, \"search\", [domain], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {import(\"@web/core/domain\").DomainListRepr} domain\n     * @param {string[]} fields\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any[]>}\n     */\n    searchRead(model, domain, fields, kwargs = {}) {\n        validateArray(\"domain\", domain);\n        if (fields) {\n            validatePrimitiveList(\"fields\", \"string\", fields);\n        }\n        return this.call(model, \"search_read\", [], { ...kwargs, domain, fields });\n    }\n\n    /**\n     * @param {string} model\n     * @param {import(\"@web/core/domain\").DomainListRepr} domain\n     * @param {any} [kwargs={}]\n     * @returns {Promise<number>}\n     */\n    searchCount(model, domain, kwargs = {}) {\n        validateArray(\"domain\", domain);\n        return this.call(model, \"search_count\", [domain], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {number[]} ids\n     * @param {any} [kwargs={}]\n     * @returns {Promise<boolean>}\n     */\n    unlink(model, ids, kwargs = {}) {\n        validatePrimitiveList(\"ids\", \"number\", ids);\n        if (!ids.length) {\n            return Promise.resolve(true);\n        }\n        return this.call(model, \"unlink\", [ids], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {import(\"@web/core/domain\").DomainListRepr} domain\n     * @param {string[]} fields\n     * @param {string[]} groupby\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any[]>}\n     */\n    webReadGroup(model, domain, fields, groupby, kwargs = {}) {\n        validateArray(\"domain\", domain);\n        validatePrimitiveList(\"fields\", \"string\", fields);\n        validatePrimitiveList(\"groupby\", \"string\", groupby);\n        return this.call(model, \"web_read_group\", [], {\n            ...kwargs,\n            groupby,\n            domain,\n            fields,\n        });\n    }\n\n    /**\n     * @param {string} model\n     * @param {number[]} ids\n     * @param {any} [kwargs={}]\n     * @param {Object} [kwargs.specification]\n     * @param {Object} [kwargs.context]\n     * @returns {Promise<any[]>}\n     */\n    webRead(model, ids, kwargs = {}) {\n        validatePrimitiveList(\"ids\", \"number\", ids);\n        return this.call(model, \"web_read\", [ids], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {import(\"@web/core/domain\").DomainListRepr} domain\n     * @param {any} [kwargs={}]\n     * @returns {Promise<any[]>}\n     */\n    webSearchRead(model, domain, kwargs = {}) {\n        validateArray(\"domain\", domain);\n        return this.call(model, \"web_search_read\", [], { ...kwargs, domain });\n    }\n\n    /**\n     * @param {string} model\n     * @param {number[]} ids\n     * @param {any} data\n     * @param {any} [kwargs={}]\n     * @returns {Promise<boolean>}\n     */\n    write(model, ids, data, kwargs = {}) {\n        validatePrimitiveList(\"ids\", \"number\", ids);\n        validateObject(\"data\", data);\n        return this.call(model, \"write\", [ids, data], kwargs);\n    }\n\n    /**\n     * @param {string} model\n     * @param {number[]} ids\n     * @param {any} data\n     * @param {any} [kwargs={}]\n     * @param {Object} [kwargs.specification]\n     * @param {Object} [kwargs.context]\n     * @returns {Promise<any[]>}\n     */\n    webSave(model, ids, data, kwargs = {}) {\n        validatePrimitiveList(\"ids\", \"number\", ids);\n        validateObject(\"data\", data);\n        return this.call(model, \"web_save\", [ids, data], kwargs);\n    }\n}\n\n/**\n * Note:\n *\n * when we will need a way to configure a rpc (for example, to setup a \"shadow\"\n * flag, or some way of not displaying errors), we can use the following api:\n *\n * this.orm = useService('orm');\n *\n * ...\n *\n * const result = await this.orm.withOption({shadow: true}).read('res.partner', [id]);\n */\nexport const ormService = {\n    async: [\n        \"call\",\n        \"create\",\n        \"nameGet\",\n        \"read\",\n        \"readGroup\",\n        \"search\",\n        \"searchRead\",\n        \"unlink\",\n        \"webSearchRead\",\n        \"write\",\n    ],\n    start() {\n        return new ORM();\n    },\n};\n\nregistry.category(\"services\").add(\"orm\", ormService);\n", "import { Component, onWillDestroy, useChildSubEnv, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { ErrorHandler } from \"@web/core/utils/components\";\n\nconst OVERLAY_ITEMS = [];\nexport const OVERLAY_SYMBOL = Symbol(\"Overlay\");\n\nclass OverlayItem extends Component {\n    static template = \"web.OverlayContainer.Item\";\n    static components = {};\n    static props = {\n        component: { type: Function },\n        props: { type: Object },\n        env: { type: Object, optional: true },\n    };\n\n    setup() {\n        this.rootRef = useRef(\"rootRef\");\n\n        OVERLAY_ITEMS.push(this);\n        onWillDestroy(() => {\n            const index = OVERLAY_ITEMS.indexOf(this);\n            OVERLAY_ITEMS.splice(index, 1);\n        });\n\n        if (this.props.env) {\n            this.__owl__.childEnv = this.props.env;\n        }\n\n        useChildSubEnv({\n            [OVERLAY_SYMBOL]: {\n                contains: (target) => this.contains(target),\n            },\n        });\n    }\n\n    get subOverlays() {\n        return OVERLAY_ITEMS.slice(OVERLAY_ITEMS.indexOf(this));\n    }\n\n    contains(target) {\n        return (\n            this.rootRef.el?.contains(target) ||\n            this.subOverlays.some((oi) => oi.rootRef.el?.contains(target))\n        );\n    }\n}\n\nexport class OverlayContainer extends Component {\n    static template = \"web.OverlayContainer\";\n    static components = { ErrorHandler, OverlayItem };\n    static props = { overlays: Object };\n\n    setup() {\n        this.root = useRef(\"root\");\n        this.state = useState({ rootEl: null });\n        useEffect(\n            () => {\n                this.state.rootEl = this.root.el;\n            },\n            () => [this.root.el]\n        );\n    }\n\n    get sortedOverlays() {\n        return sortBy(Object.values(this.props.overlays), (overlay) => overlay.sequence);\n    }\n\n    isVisible(overlay) {\n        return overlay.rootId === this.state.rootEl?.getRootNode()?.host?.id;\n    }\n\n    handleError(overlay, error) {\n        overlay.remove();\n        Promise.resolve().then(() => {\n            throw error;\n        });\n    }\n}\n", "import { reactive } from \"@odoo/owl\";\nimport { registry } from \"../registry\";\nimport { OverlayContainer } from \"./overlay_container\";\n\nconst mainComponents = registry.category(\"main_components\");\nconst services = registry.category(\"services\");\n\n/**\n * @typedef {{\n *  env?: object;\n *  onRemove?: () => void;\n *  sequence?: number;\n *  rootId?: string;\n * }} OverlayServiceAddOptions\n */\n\nexport const overlayService = {\n    start() {\n        let nextId = 0;\n        const overlays = reactive({});\n\n        mainComponents.add(\"OverlayContainer\", {\n            Component: OverlayContainer,\n            props: { overlays },\n        });\n\n        const remove = (id, onRemove = () => {}) => {\n            if (id in overlays) {\n                onRemove();\n                delete overlays[id];\n            }\n        };\n\n        /**\n         * @param {typeof Component} component\n         * @param {object} props\n         * @param {OverlayServiceAddOptions} [options]\n         * @returns {() => void}\n         */\n        const add = (component, props, options = {}) => {\n            const id = ++nextId;\n            const removeCurrentOverlay = () => remove(id, options.onRemove);\n            overlays[id] = {\n                id,\n                component,\n                env: options.env,\n                props,\n                remove: removeCurrentOverlay,\n                sequence: options.sequence ?? 50,\n                rootId: options.rootId,\n            };\n            return removeCurrentOverlay;\n        };\n\n        return { add, overlays };\n    },\n};\n\nservices.add(\"overlay\", overlayService);\n", "import { useAutofocus } from \"../utils/hooks\";\nimport { clamp } from \"../utils/numbers\";\n\nimport { Component, useExternalListener, useState, EventBus } from \"@odoo/owl\";\n\nexport const PAGER_UPDATED_EVENT = \"PAGER:UPDATED\";\nexport const pagerBus = new EventBus();\n\n/**\n * Pager\n *\n * The pager goes from 1 to total (included).\n * The current value is minimum if limit === 1 or the interval:\n *      [minimum, minimum + limit[ if limit > 1].\n * The value can be manually changed by clicking on the pager value and giving\n * an input matching the pattern: min[,max] (in which the comma can be a dash\n * or a semicolon).\n * The pager also provides two buttons to quickly change the current page (next\n * or previous).\n * @extends Component\n */\nexport class Pager extends Component {\n    static template = \"web.Pager\";\n    static defaultProps = {\n        isEditable: true,\n        withAccessKey: true,\n    };\n    static props = {\n        offset: Number,\n        limit: Number,\n        total: Number,\n        onUpdate: Function,\n        isEditable: { type: Boolean, optional: true },\n        withAccessKey: { type: Boolean, optional: true },\n        updateTotal: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.state = useState({\n            isEditing: false,\n            isDisabled: false,\n        });\n        this.inputRef = useAutofocus();\n        useExternalListener(document, \"mousedown\", this.onClickAway, { capture: true });\n    }\n\n    /**\n     * @returns {number}\n     */\n    get minimum() {\n        return this.props.offset + 1;\n    }\n    /**\n     * @returns {number}\n     */\n    get maximum() {\n        return Math.min(this.props.offset + this.props.limit, this.props.total);\n    }\n    /**\n     * @returns {string}\n     */\n    get value() {\n        const parts = [this.minimum];\n        if (this.props.limit > 1) {\n            parts.push(this.maximum);\n        }\n        return parts.join(\"-\");\n    }\n    /**\n     * Note: returns false if we received the props \"updateTotal\", as in this case we don't know\n     * the real total so we can't assert that there's a single page.\n     * @returns {boolean} true if there is only one page\n     */\n    get isSinglePage() {\n        return !this.props.updateTotal && this.minimum === 1 && this.maximum === this.props.total;\n    }\n    /**\n     * @param {-1 | 1} direction\n     */\n    async navigate(direction) {\n        let minimum = this.props.offset + this.props.limit * direction;\n        let total = this.props.total;\n        if (this.props.updateTotal && minimum < 0) {\n            // we must know the real total to be able to loop by doing \"previous\"\n            total = await this.props.updateTotal();\n        }\n        if (minimum >= total) {\n            if (!this.props.updateTotal) {\n                // only loop forward if we know the real total, otherwise let the minimum\n                // go out of range\n                minimum = 0;\n            }\n        } else if (minimum < 0 && this.props.limit === 1) {\n            minimum = total - 1;\n        } else if (minimum < 0 && this.props.limit > 1) {\n            minimum = total - (total % this.props.limit || this.props.limit);\n        }\n        this.update(minimum, this.props.limit, true);\n    }\n    /**\n     * @param {string} value\n     * @returns {{ minimum: number, maximum: number }}\n     */\n    async parse(value) {\n        let [minimum, maximum] = value.trim().split(/\\s*[-\\s,;]\\s*/);\n        minimum = parseInt(minimum, 10);\n        maximum = maximum ? parseInt(maximum, 10) : minimum;\n        if (this.props.updateTotal) {\n            // we don't know the real total, so we can't clamp\n            return { minimum: minimum - 1, maximum };\n        }\n        return {\n            minimum: clamp(minimum, 1, this.props.total) - 1,\n            maximum: clamp(maximum, 1, this.props.total),\n        };\n    }\n    /**\n     * @param {string} value\n     */\n    async setValue(value) {\n        const { minimum, maximum } = await this.parse(value);\n\n        if (!isNaN(minimum) && !isNaN(maximum) && minimum < maximum) {\n            this.update(minimum, maximum - minimum);\n        }\n    }\n    /**\n     * @param {number} offset\n     * @param {number} limit\n     * @param {Boolean} hasNavigated\n     */\n    async update(offset, limit, hasNavigated) {\n        this.state.isDisabled = true;\n        try {\n            await this.props.onUpdate({ offset, limit }, hasNavigated);\n        } finally {\n            if (this.env.isSmall) {\n                pagerBus.trigger(PAGER_UPDATED_EVENT, {\n                    value: this.value,\n                    total: this.props.total,\n                });\n            }\n            this.state.isDisabled = false;\n            this.state.isEditing = false;\n        }\n    }\n\n    async updateTotal() {\n        if (!this.state.isDisabled) {\n            this.state.isDisabled = true;\n            await this.props.updateTotal();\n            this.state.isDisabled = false;\n        }\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    onClickAway(ev) {\n        if (ev.target !== this.inputRef.el) {\n            this.state.isEditing = false;\n        }\n    }\n    onInputBlur() {\n        this.state.isEditing = false;\n    }\n    /**\n     * @param {Event} ev\n     */\n    onInputChange(ev) {\n        this.setValue(ev.target.value);\n        if (!this.state.isDisabled) {\n            ev.preventDefault();\n        }\n    }\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onInputKeydown(ev) {\n        switch (ev.key) {\n            case \"Enter\":\n                ev.preventDefault();\n                ev.stopPropagation();\n                this.setValue(ev.currentTarget.value);\n                break;\n            case \"Escape\":\n                ev.preventDefault();\n                ev.stopPropagation();\n                this.state.isEditing = false;\n                break;\n        }\n    }\n    onValueClick() {\n        if (this.props.isEditable && !this.state.isEditing && !this.state.isDisabled) {\n            if (this.inputRef.el) {\n                this.inputRef.el.focus();\n            }\n            this.state.isEditing = true;\n        }\n    }\n}\n", "import { browser } from \"../browser/browser\";\nimport { registry } from \"../registry\";\nimport { Transition } from \"../transition\";\nimport { useBus } from \"../utils/hooks\";\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { PAGER_UPDATED_EVENT, pagerBus } from \"./pager\";\n\nexport class PagerIndicator extends Component {\n    static template = \"web.PagerIndicator\";\n    static components = { Transition };\n    static props = {};\n\n    setup() {\n        this.state = useState({\n            show: false,\n            value: \"-\",\n            total: 0,\n        });\n        this.startShowTimer = null;\n        useBus(pagerBus, PAGER_UPDATED_EVENT, this.pagerUpdate);\n    }\n\n    pagerUpdate({ detail }) {\n        this.state.value = detail.value;\n        this.state.total = detail.total;\n        browser.clearTimeout(this.startShowTimer);\n        this.state.show = true;\n        this.startShowTimer = browser.setTimeout(() => {\n            this.state.show = false;\n        }, 1400);\n    }\n}\n\nregistry.category(\"main_components\").add(\"PagerIndicator\", {\n    Component: PagerIndicator,\n});\n", "import { Component, onMounted, onWillDestroy, useExternalListener, useRef } from \"@odoo/owl\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { OVERLAY_SYMBOL } from \"@web/core/overlay/overlay_container\";\nimport { usePosition } from \"@web/core/position/position_hook\";\nimport { useActiveElement } from \"@web/core/ui/ui_service\";\nimport { addClassesToElement, mergeClasses } from \"@web/core/utils/classname\";\nimport { useForwardRefToParent } from \"@web/core/utils/hooks\";\n\n/**\n * Will trigger the callback when the window is clicked, giving\n * the clicked element as parameter.\n *\n * This also handles the case where an iframe is clicked.\n *\n * @param {Function} callback\n */\nfunction useClickAway(callback) {\n    const pointerDownHandler = (event) => {\n        callback(event.composedPath()[0]);\n    };\n\n    const blurHandler = (ev) => {\n        const target = ev.relatedTarget || document.activeElement;\n        if (target?.tagName === \"IFRAME\") {\n            callback(target);\n        }\n    };\n\n    useExternalListener(window, \"pointerdown\", pointerDownHandler, { capture: true });\n    useExternalListener(window, \"blur\", blurHandler, { capture: true });\n}\n\nconst POPOVERS = new WeakMap();\n/**\n * Can be used to retrieve the popover element for a given target.\n * @param {HTMLElement} target\n * @returns {HTMLElement | undefined} the popover element if it exists\n */\nexport function getPopoverForTarget(target) {\n    return POPOVERS.get(target);\n}\n\nexport class Popover extends Component {\n    static template = \"web.Popover\";\n    static defaultProps = {\n        animation: true,\n        arrow: true,\n        class: \"\",\n        closeOnClickAway: () => true,\n        closeOnEscape: true,\n        componentProps: {},\n        fixedPosition: false,\n        position: \"bottom\",\n        setActiveElement: false,\n    };\n    static props = {\n        // Main props\n        component: { type: Function },\n        componentProps: { optional: true, type: Object },\n        target: {\n            validate: (target) => {\n                // target may be inside an iframe, so get the Element constructor\n                // to test against from its owner document's default view\n                const Element = target?.ownerDocument?.defaultView?.Element;\n                return (\n                    (Boolean(Element) &&\n                        (target instanceof Element || target instanceof window.Element)) ||\n                    (typeof target === \"object\" && target?.constructor?.name?.endsWith(\"Element\"))\n                );\n            },\n        },\n\n        // Styling and semantical props\n        animation: { optional: true, type: Boolean },\n        arrow: { optional: true, type: Boolean },\n        class: { optional: true },\n        role: { optional: true, type: String },\n\n        // Positioning props\n        fixedPosition: { optional: true, type: Boolean },\n        holdOnHover: { optional: true, type: Boolean },\n        onPositioned: { optional: true, type: Function },\n        position: {\n            optional: true,\n            type: String,\n            validate: (p) => {\n                const [d, v = \"middle\"] = p.split(\"-\");\n                return (\n                    [\"top\", \"bottom\", \"left\", \"right\"].includes(d) &&\n                    [\"start\", \"middle\", \"end\", \"fit\"].includes(v)\n                );\n            },\n        },\n\n        // Control props\n        close: { optional: true, type: Function },\n        closeOnClickAway: { optional: true, type: Function },\n        closeOnEscape: { optional: true, type: Boolean },\n        setActiveElement: { optional: true, type: Boolean },\n\n        // Technical props\n        ref: { optional: true, type: Function },\n        slots: { optional: true, type: Object },\n    };\n\n    static animationTime = 200;\n    setup() {\n        if (this.props.setActiveElement) {\n            useActiveElement(\"ref\");\n        }\n\n        useForwardRefToParent(\"ref\");\n        this.popoverRef = useRef(\"ref\");\n\n        let shouldAnimate = this.props.animation;\n        this.position = usePosition(\"ref\", () => this.props.target, {\n            onPositioned: (el, solution) => {\n                (this.props.onPositioned || this.onPositioned.bind(this))(el, solution);\n                if (this.props.arrow && this.props.onPositioned) {\n                    this.onPositioned.bind(this)(el, solution);\n                }\n\n                // opening animation\n                if (shouldAnimate) {\n                    shouldAnimate = false; // animate only once\n                    const transform = {\n                        top: [\"translateY(-5%)\", \"translateY(0)\"],\n                        right: [\"translateX(5%)\", \"translateX(0)\"],\n                        bottom: [\"translateY(5%)\", \"translateY(0)\"],\n                        left: [\"translateX(-5%)\", \"translateX(0)\"],\n                    }[solution.direction];\n                    this.position.lock();\n                    const animation = el.animate(\n                        { opacity: [0, 1], transform },\n                        this.constructor.animationTime\n                    );\n                    animation.finished.then(this.position.unlock);\n                }\n\n                if (this.props.fixedPosition) {\n                    // Prevent further positioning updates if fixed position is wanted\n                    this.position.lock();\n                }\n            },\n            position: this.props.position,\n        });\n\n        if (this.props.target.isConnected) {\n            useClickAway((target) => this.onClickAway(target));\n\n            if (this.props.closeOnEscape) {\n                useHotkey(\"escape\", () => this.props.close());\n            }\n            const targetObserver = new MutationObserver(this.onTargetMutate.bind(this));\n            targetObserver.observe(this.props.target.parentElement, { childList: true });\n            onWillDestroy(() => targetObserver.disconnect());\n        } else {\n            this.props.close();\n        }\n        onMounted(() => POPOVERS.set(this.props.target, this.popoverRef.el));\n        onWillDestroy(() => POPOVERS.delete(this.props.target));\n    }\n\n    get defaultClassObj() {\n        return mergeClasses(\n            \"o_popover popover mw-100\",\n            { \"o-popover--with-arrow\": this.props.arrow },\n            this.props.class\n        );\n    }\n\n    isInside(target) {\n        return (\n            this.props.target.contains(target) ||\n            this.popoverRef.el.contains(target) ||\n            this.env[OVERLAY_SYMBOL]?.contains(target)\n        );\n    }\n\n    onClickAway(target) {\n        if (this.props.closeOnClickAway(target) && !this.isInside(target)) {\n            this.props.close();\n        }\n    }\n\n    onTargetMutate() {\n        if (!this.props.target.isConnected) {\n            this.props.close();\n        }\n    }\n\n    onPositioned(el, { direction, variant }) {\n        const position = `${direction[0]}${variant[0]}`;\n\n        // reset all popover classes\n        el.classList = [];\n        const directionMap = {\n            top: \"top\",\n            bottom: \"bottom\",\n            left: \"start\",\n            right: \"end\",\n        };\n        addClassesToElement(\n            el,\n            this.defaultClassObj,\n            `bs-popover-${directionMap[direction]}`,\n            `o-popover-${direction}`,\n            `o-popover--${position}`\n        );\n\n        if (this.props.arrow) {\n            const arrowEl = el.querySelector(\":scope > .popover-arrow\");\n            // reset all arrow classes\n            arrowEl.className = \"popover-arrow\";\n            switch (position) {\n                case \"tm\": // top-middle\n                case \"bm\": // bottom-middle\n                case \"tf\": // top-fit\n                case \"bf\": // bottom-fit\n                    arrowEl.classList.add(\"start-0\", \"end-0\", \"mx-auto\");\n                    break;\n                case \"lm\": // left-middle\n                case \"rm\": // right-middle\n                case \"lf\": // left-fit\n                case \"rf\": // right-fit\n                    arrowEl.classList.add(\"top-0\", \"bottom-0\", \"my-auto\");\n                    break;\n                case \"ts\": // top-start\n                case \"bs\": // bottom-start\n                    arrowEl.classList.add(\"end-auto\");\n                    break;\n                case \"te\": // top-end\n                case \"be\": // bottom-end\n                    arrowEl.classList.add(\"start-auto\");\n                    break;\n                case \"ls\": // left-start\n                case \"rs\": // right-start\n                    arrowEl.classList.add(\"bottom-auto\");\n                    break;\n                case \"le\": // left-end\n                case \"re\": // right-end\n                    arrowEl.classList.add(\"top-auto\");\n                    break;\n            }\n        }\n    }\n}\n", "import { onWillUnmount, status, useComponent } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {import(\"@web/core/popover/popover_service\").PopoverServiceAddFunction} PopoverServiceAddFunction\n * @typedef {import(\"@web/core/popover/popover_service\").PopoverServiceAddOptions} PopoverServiceAddOptions\n */\n\n/**\n * @typedef PopoverHookReturnType\n * @property {(target: string | HTMLElement, props: object) => void} open\n *  - Signals the manager to open the configured popover\n *    component on the target, with the given props.\n * @property {() => void} close\n *  - Signals the manager to remove the popover.\n * @property {boolean} isOpen\n *  - Whether the popover is currently open.\n */\n\n/**\n * @param {PopoverServiceAddFunction} addFn\n * @param {typeof import(\"@odoo/owl\").Component} component\n * @param {PopoverServiceAddOptions} options\n * @returns {PopoverHookReturnType}\n */\nexport function makePopover(addFn, component, options) {\n    let removeFn = null;\n    function close() {\n        removeFn?.();\n    }\n    return {\n        open(target, props) {\n            close();\n            const newOptions = Object.create(options);\n            newOptions.onClose = () => {\n                removeFn = null;\n                options.onClose?.();\n            };\n            removeFn = addFn(target, component, props, newOptions);\n        },\n        close,\n        get isOpen() {\n            return Boolean(removeFn);\n        },\n    };\n}\n\n/**\n * Manages a component to be used as a popover.\n *\n * @param {typeof import(\"@odoo/owl\").Component} component\n * @param {PopoverServiceAddOptions} [options]\n * @returns {PopoverHookReturnType}\n */\nexport function usePopover(component, options = {}) {\n    const popoverService = useService(\"popover\");\n    const owner = useComponent();\n    const newOptions = Object.create(options);\n    newOptions.onClose = () => {\n        if (status(owner) !== \"destroyed\") {\n            options.onClose?.();\n        }\n    };\n    const popover = makePopover(popoverService.add, component, newOptions);\n    onWillUnmount(popover.close);\n    return popover;\n}\n", "import { markRaw } from \"@odoo/owl\";\nimport { Popover } from \"@web/core/popover/popover\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * @typedef {{\n *   animation?: Boolean;\n *   arrow?: Boolean;\n *   closeOnClickAway?: boolean | (target: HTMLElement) => boolean;\n *   closeOnEscape?: boolean;\n *   env?: object;\n *   fixedPosition?: boolean;\n *   onClose?: () => void;\n *   onPositioned?: import(\"@web/core/position/position_hook\").UsePositionOptions[\"onPositioned\"];\n *   popoverClass?: string;\n *   popoverRole?: string;\n *   position?: import(\"@web/core/position/position_hook\").UsePositionOptions[\"position\"];\n *   ref?: Function;\n * }} PopoverServiceAddOptions\n *\n * @typedef {ReturnType<popoverService[\"start\"]>[\"add\"]} PopoverServiceAddFunction\n */\n\nexport const popoverService = {\n    dependencies: [\"overlay\"],\n    start(_, { overlay }) {\n        /**\n         * Signals the manager to add a popover.\n         *\n         * @param {HTMLElement} target\n         * @param {typeof import(\"@odoo/owl\").Component} component\n         * @param {object} [props]\n         * @param {PopoverServiceAddOptions} [options]\n         * @returns {() => void}\n         */\n        const add = (target, component, props = {}, options = {}) => {\n            const closeOnClickAway =\n                typeof options.closeOnClickAway === \"function\"\n                    ? options.closeOnClickAway\n                    : () => options.closeOnClickAway ?? true;\n            const remove = overlay.add(\n                Popover,\n                {\n                    target,\n                    close: () => remove(),\n                    closeOnClickAway,\n                    closeOnEscape: options.closeOnEscape,\n                    component,\n                    componentProps: markRaw(props),\n                    ref: options.ref,\n                    class: options.popoverClass,\n                    animation: options.animation,\n                    arrow: options.arrow,\n                    role: options.popoverRole,\n                    position: options.position,\n                    onPositioned: options.onPositioned,\n                    fixedPosition: options.fixedPosition,\n                    holdOnHover: options.holdOnHover,\n                    setActiveElement: options.setActiveElement ?? true,\n                },\n                {\n                    env: options.env,\n                    onRemove: options.onClose,\n                    rootId: target.getRootNode()?.host?.id,\n                }\n            );\n\n            return remove;\n        };\n\n        return { add };\n    },\n};\n\nregistry.category(\"services\").add(\"popover\", popoverService);\n", "import { reposition } from \"@web/core/position/utils\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { useThrottleForAnimation } from \"@web/core/utils/timing\";\nimport {\n    EventBus,\n    onWillDestroy,\n    useChildSubEnv,\n    useComponent,\n    useEffect,\n    useRef,\n} from \"@odoo/owl\";\n\n/**\n * @typedef {import(\"@web/core/position/utils\").ComputePositionOptions} ComputePositionOptions\n * @typedef {import(\"@web/core/position/utils\").PositioningSolution} PositioningSolution\n *\n * @typedef {Object} UsePositionOptionsExtensionType\n * @property {(popperElement: HTMLElement, solution: PositioningSolution) => void} [onPositioned]\n *  callback called when the positioning is done.\n * @typedef {ComputePositionOptions & UsePositionOptionsExtensionType} UsePositionOptions\n *\n * @typedef PositioningControl\n * @property {() => void} lock prevents further positioning updates\n * @property {() => void} unlock allows further positioning updates (triggers an update right away)\n */\n\n/** @type {UsePositionOptions} */\nconst DEFAULTS = {\n    margin: 0,\n    position: \"bottom\",\n};\n\nconst POSITION_BUS = Symbol(\"position-bus\");\n\n/**\n * Makes sure that the `popper` element is always\n * placed at `position` from the `target` element.\n * If doing so the `popper` element is clipped off `container`,\n * sensible fallback positions are tried.\n * If all of fallback positions are also clipped off `container`,\n * the original position is used.\n *\n * Note: The popper element should be indicated in your template\n *       with a t-ref reference matching the refName argument.\n *\n * @param {string} refName\n *  name of the reference to the popper element in the template.\n * @param {() => HTMLElement} getTarget\n * @param {UsePositionOptions} [options={}] the options to be used for positioning\n * @returns {PositioningControl}\n *  control object to lock/unlock the positioning.\n */\nexport function usePosition(refName, getTarget, options = {}) {\n    const ref = useRef(refName);\n    let lock = false;\n    const update = () => {\n        const targetEl = getTarget();\n        if (!ref.el || !targetEl?.isConnected || lock) {\n            // No compute needed\n            return;\n        }\n        const repositionOptions = { ...DEFAULTS, ...omit(options, \"onPositioned\") };\n        const solution = reposition(ref.el, targetEl, repositionOptions);\n        options.onPositioned?.(ref.el, solution);\n    };\n\n    const component = useComponent();\n    const bus = component.env[POSITION_BUS] || new EventBus();\n\n    let executingUpdate = false;\n    const batchedUpdate = async () => {\n        // not same as batch, here we're executing once and then awaiting\n        if (!executingUpdate) {\n            executingUpdate = true;\n            update();\n            await Promise.resolve();\n            executingUpdate = false;\n        }\n    };\n    bus.addEventListener(\"update\", batchedUpdate);\n    onWillDestroy(() => bus.removeEventListener(\"update\", batchedUpdate));\n\n    const isTopmost = !(POSITION_BUS in component.env);\n    if (isTopmost) {\n        useChildSubEnv({ [POSITION_BUS]: bus });\n    }\n\n    const throttledUpdate = useThrottleForAnimation(() => bus.trigger(\"update\"));\n    useEffect(() => {\n        // Reposition\n        bus.trigger(\"update\");\n\n        if (isTopmost) {\n            // Attach listeners to keep the positioning up to date\n            const scrollListener = (e) => {\n                if (ref.el?.contains(e.target)) {\n                    // In case the scroll event occurs inside the popper, do not reposition\n                    return;\n                }\n                throttledUpdate();\n            };\n            const targetDocument = getTarget()?.ownerDocument;\n            targetDocument?.addEventListener(\"scroll\", scrollListener, { capture: true });\n            targetDocument?.addEventListener(\"load\", throttledUpdate, { capture: true });\n            window.addEventListener(\"resize\", throttledUpdate);\n            return () => {\n                targetDocument?.removeEventListener(\"scroll\", scrollListener, { capture: true });\n                targetDocument?.removeEventListener(\"load\", throttledUpdate, { capture: true });\n                window.removeEventListener(\"resize\", throttledUpdate);\n            };\n        }\n    });\n\n    return {\n        lock: () => {\n            lock = true;\n        },\n        unlock: () => {\n            lock = false;\n            bus.trigger(\"update\");\n        },\n    };\n}\n", "import { localization } from \"@web/core/l10n/localization\";\n\n/**\n * @typedef {\"top\" | \"left\" | \"bottom\" | \"right\"} Direction\n * @typedef {\"start\" | \"middle\" | \"end\" | \"fit\"} Variant\n *\n * @typedef {{[direction in Direction]: string}} DirectionFlipOrder\n *  string values should match regex /^[tbrl]+$/m\n *\n * @typedef {{[variant in Variant]: string}} VariantFlipOrder\n *  string values should match regex /^[smef]+$/m\n *\n * @typedef {{\n *  top: number,\n *  left: number,\n *  direction: Direction,\n *  variant: Variant,\n * }} PositioningSolution\n *\n * @typedef ComputePositionOptions\n * @property {HTMLElement | () => HTMLElement} [container] container element\n * @property {number} [margin=0]\n *  margin in pixels between the popper and the target.\n * @property {Direction | `${Direction}-${Variant}`} [position=\"bottom\"]\n *  position of the popper relative to the target\n */\n\n/** @type {{[d: string]: Direction}} */\nconst DIRECTIONS = { t: \"top\", r: \"right\", b: \"bottom\", l: \"left\" };\n/** @type {{[v: string]: Variant}} */\nconst VARIANTS = { s: \"start\", m: \"middle\", e: \"end\", f: \"fit\" };\n/** @type DirectionFlipOrder */\nconst DIRECTION_FLIP_ORDER = { top: \"tbrl\", right: \"rltb\", bottom: \"btrl\", left: \"lrbt\" };\n/** @type VariantFlipOrder */\nconst VARIANT_FLIP_ORDER = { start: \"sme\", middle: \"mse\", end: \"ems\", fit: \"f\" };\n/** @type DirectionFlipOrder */\nconst FIT_FLIP_ORDER = { top: \"tb\", right: \"rl\", bottom: \"bt\", left: \"lr\" };\n\n/**\n * @param {HTMLElement} popperEl\n * @param {HTMLElement} targetEl\n * @returns {HTMLIFrameElement?}\n */\nfunction getIFrame(popperEl, targetEl) {\n    return [...popperEl.ownerDocument.getElementsByTagName(\"iframe\")].find((iframe) =>\n        iframe.contentDocument?.contains(targetEl)\n    );\n}\n\n/**\n * Returns the best positioning solution staying in the container or falls back\n * to the requested position.\n * The positioning data used to determine each possible position is based on\n * the target, popper, and container sizes.\n * Particularly, a popper must not overflow the container in any direction.\n * The popper will stay at `margin` distance from its target. One could also\n * use the CSS margins of the popper element to achieve the same result.\n *\n * Pre-condition: the popper element must have a fixed positioning\n *                with top and left set to 0px.\n *\n * @param {HTMLElement} popper\n * @param {HTMLElement} target\n * @param {ComputePositionOptions} options\n * @returns {PositioningSolution} the best positioning solution, relative to\n *                                the containing block of the popper.\n *                                => can be applied to popper.style.(top|left)\n */\nfunction computePosition(popper, target, { container, margin, position }) {\n    // Retrieve directions and variants\n    let [direction, variant = \"middle\"] = position.split(\"-\");\n    if (localization.direction === \"rtl\") {\n        if ([\"left\", \"right\"].includes(direction)) {\n            direction = direction === \"left\" ? \"right\" : \"left\";\n        } else if ([\"start\", \"end\"].includes(variant)) {\n            // here direction is either \"top\" or \"bottom\"\n            variant = variant === \"start\" ? \"end\" : \"start\";\n        }\n    }\n    const directions =\n        variant === \"fit\" ? FIT_FLIP_ORDER[direction] : DIRECTION_FLIP_ORDER[direction];\n    const variants = VARIANT_FLIP_ORDER[variant];\n\n    // Retrieve container\n    if (!container) {\n        container = popper.ownerDocument.documentElement;\n    } else if (typeof container === \"function\") {\n        container = container();\n    }\n\n    // Account for popper actual margins\n    const popperStyle = getComputedStyle(popper);\n    const { marginTop, marginLeft, marginRight, marginBottom } = popperStyle;\n    const popMargins = {\n        top: parseFloat(marginTop),\n        left: parseFloat(marginLeft),\n        right: parseFloat(marginRight),\n        bottom: parseFloat(marginBottom),\n    };\n\n    // IFrame\n    const shouldAccountForIFrame = popper.ownerDocument !== target.ownerDocument;\n    const iframe = shouldAccountForIFrame ? getIFrame(popper, target) : null;\n\n    // Boxes\n    const popBox = popper.getBoundingClientRect();\n    const targetBox = target.getBoundingClientRect();\n    const contBox = container.getBoundingClientRect();\n    const iframeBox = iframe?.getBoundingClientRect() ?? { top: 0, left: 0 };\n\n    const containerIsHTMLNode = container === container.ownerDocument.firstElementChild;\n\n    // Compute positioning data\n    const directionsData = {\n        t: iframeBox.top + targetBox.top - popMargins.bottom - margin - popBox.height,\n        b: iframeBox.top + targetBox.bottom + popMargins.top + margin,\n        r: iframeBox.left + targetBox.right + popMargins.left + margin,\n        l: iframeBox.left + targetBox.left - popMargins.right - margin - popBox.width,\n    };\n    const variantsData = {\n        vf: iframeBox.left + targetBox.left,\n        vs: iframeBox.left + targetBox.left + popMargins.left,\n        vm: iframeBox.left + targetBox.left + targetBox.width / 2 - popBox.width / 2,\n        ve: iframeBox.left + targetBox.right - popMargins.right - popBox.width,\n        hf: iframeBox.top + targetBox.top,\n        hs: iframeBox.top + targetBox.top + popMargins.top,\n        hm: iframeBox.top + targetBox.top + targetBox.height / 2 - popBox.height / 2,\n        he: iframeBox.top + targetBox.bottom - popMargins.bottom - popBox.height,\n    };\n\n    function getPositioningData(d = directions[0], v = variants[0], containerRestricted = false) {\n        const vertical = [\"t\", \"b\"].includes(d);\n        const variantPrefix = vertical ? \"v\" : \"h\";\n        const directionValue = directionsData[d];\n        const variantValue = variantsData[variantPrefix + v];\n\n        if (containerRestricted) {\n            const [directionSize, variantSize] = vertical\n                ? [popBox.height, popBox.width]\n                : [popBox.width, popBox.height];\n            let [directionMin, directionMax] = vertical\n                ? [contBox.top, contBox.bottom]\n                : [contBox.left, contBox.right];\n            let [variantMin, variantMax] = vertical\n                ? [contBox.left, contBox.right]\n                : [contBox.top, contBox.bottom];\n\n            if (containerIsHTMLNode) {\n                if (vertical) {\n                    directionMin += container.scrollTop;\n                    directionMax += container.scrollTop;\n                } else {\n                    variantMin += container.scrollTop;\n                    variantMax += container.scrollTop;\n                }\n            }\n\n            // Abort if outside container boundaries\n            const directionOverflow =\n                Math.ceil(directionValue) < Math.floor(directionMin) ||\n                Math.floor(directionValue + directionSize) > Math.ceil(directionMax);\n            const variantOverflow =\n                Math.ceil(variantValue) < Math.floor(variantMin) ||\n                Math.floor(variantValue + variantSize) > Math.ceil(variantMax);\n            if (directionOverflow || variantOverflow) {\n                return null;\n            }\n        }\n\n        const positioning = vertical\n            ? { top: directionValue, left: variantValue }\n            : { top: variantValue, left: directionValue };\n        return {\n            // Subtract the offsets of the containing block (relative to the\n            // viewport). It can be done like that because the style top and\n            // left were reset to 0px in `reposition`\n            // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n            top: positioning.top - popBox.top,\n            left: positioning.left - popBox.left,\n            direction: DIRECTIONS[d],\n            variant: VARIANTS[v],\n        };\n    }\n\n    // Find best solution\n    for (const d of directions) {\n        for (const v of variants) {\n            const match = getPositioningData(d, v, true);\n            if (match) {\n                // Position match have been found.\n                return match;\n            }\n        }\n    }\n\n    // Fallback to default position if no best solution found\n    return getPositioningData();\n}\n\n/**\n * Repositions the popper element relatively to the target element (according to options).\n * The positioning strategy is always a fixed positioning with top and left.\n *\n * The positioning solution is returned by the `computePosition` function.\n * It will get applied to the popper element and then returned for convenience.\n *\n * @param {HTMLElement} popper\n * @param {HTMLElement} target\n * @param {ComputePositionOptions} options\n * @returns {PositioningSolution} the applied positioning solution.\n */\nexport function reposition(popper, target, options) {\n    // Reset popper style\n    popper.style.position = \"fixed\";\n    popper.style.top = \"0px\";\n    popper.style.left = \"0px\";\n\n    // Compute positioning solution\n    const solution = computePosition(popper, target, options);\n\n    // Apply it\n    const { top, left, direction, variant } = solution;\n    popper.style.top = `${top}px`;\n    popper.style.left = `${left}px`;\n    if (variant === \"fit\") {\n        const styleProperty = [\"top\", \"bottom\"].includes(direction) ? \"width\" : \"height\";\n        popper.style[styleProperty] = target.getBoundingClientRect()[styleProperty] + \"px\";\n    }\n\n    return solution;\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { isIOS } from \"@web/core/browser/feature_detection\";\n\nexport class InstallPrompt extends Component {\n    static props = {\n        close: true,\n        onClose: { type: Function },\n    };\n    static components = {\n        Dialog,\n    };\n    static template = \"web.InstallPrompt\";\n\n    get isMobileSafari() {\n        return isIOS();\n    }\n\n    onClose() {\n        this.props.close();\n        this.props.onClose();\n    }\n}\n", "import { reactive } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport {\n    isDisplayStandalone,\n    isIOS,\n    isMacOS,\n    isBrowserSafari,\n} from \"@web/core/browser/feature_detection\";\nimport { get } from \"@web/core/network/http_service\";\nimport { registry } from \"@web/core/registry\";\nimport { InstallPrompt } from \"./install_prompt\";\n\nconst serviceRegistry = registry.category(\"services\");\n\n/* Ideally, the service would directly add the event listener. Unfortunately, it happens sometimes that\n * the browser would trigger the event before the webclient (services, components, etc.) is even ready.\n * In that case, we have to get this event as soon as possible. The service can then verify if the event\n * is already stored in this variable, or add an event listener itself, to make sure the `_handleBeforeInstallPrompt`\n * function is called at the right moment, and can give the correct information to the service.\n */\nlet BEFOREINSTALLPROMPT_EVENT;\nlet REGISTER_BEFOREINSTALLPROMPT_EVENT;\n\nbrowser.addEventListener(\"beforeinstallprompt\", (ev) => {\n    // This event is only triggered by the browser when the native prompt to install can be shown\n    // This excludes incognito tabs, as well as visiting the website while the app is installed\n    ev.preventDefault();\n    if (REGISTER_BEFOREINSTALLPROMPT_EVENT) {\n        // service has been started before the event was triggered, update the service\n        return REGISTER_BEFOREINSTALLPROMPT_EVENT(ev);\n    } else {\n        // store the event for later use\n        BEFOREINSTALLPROMPT_EVENT = ev;\n    }\n});\n\nconst pwaService = {\n    dependencies: [\"dialog\"],\n    start(env, { dialog }) {\n        let _manifest;\n        let nativePrompt;\n\n        const state = reactive({\n            canPromptToInstall: false,\n            isAvailable: false,\n            isScopedApp: browser.location.href.includes(\"/scoped_app\"),\n            isSupportedOnBrowser: false,\n            startUrl: \"/odoo\",\n            decline,\n            getManifest,\n            hasScopeBeenInstalled,\n            show,\n        });\n\n        function _getInstallationState(scope = state.startUrl) {\n            const installationState = browser.localStorage.getItem(\"pwaService.installationState\");\n            return installationState ? JSON.parse(installationState)[scope] : \"\";\n        }\n\n        function _setInstallationState(value) {\n            const ls = JSON.parse(\n                browser.localStorage.getItem(\"pwaService.installationState\") || \"{}\"\n            );\n            ls[state.startUrl] = value;\n            browser.localStorage.setItem(\"pwaService.installationState\", JSON.stringify(ls));\n        }\n\n        function _removeInstallationState() {\n            const ls = JSON.parse(browser.localStorage.getItem(\"pwaService.installationState\"));\n            delete ls[state.startUrl];\n            browser.localStorage.setItem(\"pwaService.installationState\", JSON.stringify(ls));\n        }\n\n        if (state.isScopedApp) {\n            if (browser.location.pathname === \"/scoped_app\") {\n                // Installation page, use the path parameter in the URL\n                state.startUrl = \"/\" + new URL(browser.location.href).searchParams.get(\"path\");\n            } else {\n                state.startUrl = browser.location.pathname;\n            }\n        }\n\n        // The PWA can only be installed if the app is not already launched (display-mode standalone)\n        // For Apple devices, PWA are supported on any mobile version of Safari, or in desktop since version 17\n        // On Safari devices, the check is also done on the display-mode and we rely on the installationState to\n        // decide whether we must show the prompt or not\n        state.isSupportedOnBrowser =\n            browser.BeforeInstallPromptEvent !== undefined ||\n            (isBrowserSafari() &&\n                !isDisplayStandalone() &&\n                (isIOS() ||\n                    (isMacOS() && browser.navigator.userAgent.match(/Version\\/(\\d+)/)[1] >= 17)));\n\n        const installationState = _getInstallationState();\n\n        if (state.isSupportedOnBrowser) {\n            if (BEFOREINSTALLPROMPT_EVENT) {\n                _handleBeforeInstallPrompt(BEFOREINSTALLPROMPT_EVENT, installationState);\n                BEFOREINSTALLPROMPT_EVENT = null; // clear this variable as it is no longer useful\n            }\n            // If a user declines the prompt, the browser would triggered it once again. We must be able to catch it\n            REGISTER_BEFOREINSTALLPROMPT_EVENT = (ev) => {\n                _handleBeforeInstallPrompt(ev, installationState);\n            };\n            if (isBrowserSafari()) {\n                // since those platforms don't rely on the beforeinstallprompt event, we handle it ourselves\n                state.canPromptToInstall = installationState !== \"dismissed\";\n                state.isAvailable = true;\n            }\n        }\n\n        function _handleBeforeInstallPrompt(ev, installationState) {\n            nativePrompt = ev;\n            if (installationState === \"accepted\") {\n                // If this event is triggered with the installationState stored, it means that the app has been\n                // removed since its installation. The prompt can be displayed, and the installation state is reset.\n                if (!isDisplayStandalone()) {\n                    // In Scoped Apps, the event might be triggered if a manifest with a different scope is available\n                    _removeInstallationState();\n                }\n            }\n            state.canPromptToInstall = installationState !== \"dismissed\";\n            state.isAvailable = true;\n        }\n\n        async function getManifest() {\n            if (!_manifest) {\n                const manifest = await get(\n                    document.querySelector(\"link[rel=manifest\")?.getAttribute(\"href\"),\n                    \"text\"\n                );\n                _manifest = JSON.parse(manifest);\n            }\n            return _manifest;\n        }\n\n        // This function don't guarantee the scope is still currently installed on the device\n        // The only way to know that is by relying on the BeforeInstallPrompt event from the\n        // page linking the app manifest. This only serves to indicate that the app has previously\n        // been installed\n        function hasScopeBeenInstalled(scope) {\n            return _getInstallationState(scope) === \"accepted\";\n        }\n\n        async function show({ onDone } = {}) {\n            if (!state.isAvailable) {\n                return;\n            }\n            if (nativePrompt) {\n                const res = await nativePrompt.prompt();\n                _setInstallationState(res.outcome);\n                state.canPromptToInstall = false;\n                if (onDone) {\n                    onDone(res);\n                }\n            } else if (isBrowserSafari()) {\n                // since those platforms don't support a native installation prompt yet, we\n                // show a custom dialog to explain how to pin the app to the application menu\n                dialog.add(InstallPrompt, {\n                    onClose: () => {\n                        if (onDone) {\n                            onDone({});\n                        }\n                        this.decline();\n                    },\n                });\n            }\n        }\n\n        function decline() {\n            _setInstallationState(\"dismissed\");\n            state.canPromptToInstall = false;\n        }\n\n        return state;\n    },\n};\nserviceRegistry.add(\"pwa\", pwaService);\n", "import { evaluate } from \"./py_interpreter\";\nimport { parse } from \"./py_parser\";\nimport { tokenize } from \"./py_tokenizer\";\n\nexport { evaluate } from \"./py_interpreter\";\nexport { parse } from \"./py_parser\";\nexport { tokenize } from \"./py_tokenizer\";\nexport { formatAST } from \"./py_utils\";\n\n/**\n * @typedef { import(\"./py_tokenizer\").Token } Token\n * @typedef { import(\"./py_parser\").AST } AST\n */\n\n/**\n * Parses an expression into a valid AST representation\n\n * @param {string} expr\n * @returns { AST }\n */\nexport function parseExpr(expr) {\n    const tokens = tokenize(expr);\n    return parse(tokens);\n}\n\n/**\n * Evaluates a python expression\n *\n * @param {string} expr\n * @param {Object} [context]\n * @returns {any}\n */\nexport function evaluateExpr(expr, context = {}) {\n    let ast;\n    try {\n        ast = parseExpr(expr);\n    } catch (error) {\n        throw new EvalError(`Can not parse python expression: (${expr})\\nError: ${error.message}`);\n    }\n    try {\n        return evaluate(ast, context);\n    } catch (error) {\n        throw new EvalError(`Can not evaluate python expression: (${expr})\\nError: ${error.message}`);\n    }\n}\n\n/**\n * Evaluates a python expression to return a boolean.\n *\n * @param {string} expr\n * @param {Object} [context]\n * @returns {any}\n */\nexport function evaluateBooleanExpr(expr, context = {}) {\n    if (!expr || expr === 'False' || expr === '0') {\n        return false;\n    }\n    if (expr === 'True' || expr === '1') {\n        return true;\n    }\n    return evaluateExpr(`bool(${expr})`, context);\n}\n", "import { PyDate, PyDateTime, PyRelativeDelta, PyTime, PyTimeDelta } from \"./py_date\";\n\nexport class EvaluationError extends Error {}\n\n/**\n * @param {any} iterable\n * @param {Function} func\n */\nexport function execOnIterable(iterable, func) {\n    if (iterable === null) {\n        // new Set(null) is fine in js but set(None) (-> new Set(null))\n        // is not in Python\n        throw new EvaluationError(`value not iterable`);\n    }\n    if (typeof iterable === \"object\" && !Array.isArray(iterable) && !(iterable instanceof Set)) {\n        // dicts are considered as iterable in Python\n        iterable = Object.keys(iterable);\n    }\n    if (typeof iterable?.[Symbol.iterator] !== \"function\") {\n        // rules out undefined and other values not iterable\n        throw new EvaluationError(`value not iterable`);\n    }\n    return func(iterable);\n}\n\nexport const BUILTINS = {\n    /**\n     * @param {any} value\n     * @returns {boolean}\n     */\n    bool(value) {\n        switch (typeof value) {\n            case \"number\":\n                return value !== 0;\n            case \"string\":\n                return value !== \"\";\n            case \"boolean\":\n                return value;\n            case \"object\":\n                if (value === null || value === undefined) {\n                    return false;\n                }\n                if (value.isTrue) {\n                    return value.isTrue();\n                }\n                if (value instanceof Array) {\n                    return !!value.length;\n                }\n                if (value instanceof Set) {\n                    return !!value.size;\n                }\n                return Object.keys(value).length !== 0;\n        }\n        return true;\n    },\n\n    set(iterable) {\n        if (arguments.length > 2) {\n            // we always receive at least one argument: kwargs (return fnValue(...args, kwargs); in FunctionCall case)\n            throw new EvaluationError(\n                `set expected at most 1 argument, got (${arguments.length - 1}`\n            );\n        }\n        return execOnIterable(iterable, (iterable) => {\n            return new Set(iterable);\n        });\n    },\n\n    max(...args) {\n        // kwargs are not supported by Math.max.\n        return Math.max(...args.slice(0, -1));\n    },\n\n    min(...args) {\n        // kwargs are not supported by Math.min.\n        return Math.min(...args.slice(0, -1));\n    },\n\n    time: {\n        strftime(format) {\n            return PyDateTime.now().strftime(format);\n        },\n    },\n\n    context_today() {\n        return PyDate.today();\n    },\n\n    get current_date() {\n        // deprecated: today should be prefered\n        return this.today;\n    },\n\n    get today() {\n        return PyDate.today().strftime(\"%Y-%m-%d\");\n    },\n\n    get now() {\n        return PyDateTime.now().strftime(\"%Y-%m-%d %H:%M:%S\");\n    },\n\n    datetime: {\n        time: PyTime,\n        timedelta: PyTimeDelta,\n        datetime: PyDateTime,\n        date: PyDate,\n    },\n\n    relativedelta: PyRelativeDelta,\n\n    true: true,\n    false: false,\n};\n", "import { parseArgs } from \"./py_parser\";\n\n// -----------------------------------------------------------------------------\n// Errors\n// -----------------------------------------------------------------------------\n\nexport class AssertionError extends Error {}\nexport class ValueError extends Error {}\nexport class NotSupportedError extends Error {}\n\n// -----------------------------------------------------------------------------\n// helpers\n// -----------------------------------------------------------------------------\n\nfunction fmt2(n) {\n    return String(n).padStart(2, \"0\");\n}\nfunction fmt4(n) {\n    return String(n).padStart(4, \"0\");\n}\n\n/**\n * computes (Math.floor(a/b), a%b and passes that to the callback.\n *\n * returns the callback's result\n */\nfunction divmod(a, b, fn) {\n    let mod = a % b;\n    // in python, sign(a % b) === sign(b). Not in JS. If wrong side, add a\n    // round of b\n    if ((mod > 0 && b < 0) || (mod < 0 && b > 0)) {\n        mod += b;\n    }\n    return fn(Math.floor(a / b), mod);\n}\n\nfunction assert(bool, message = \"AssertionError\") {\n    if (!bool) {\n        throw new AssertionError(message);\n    }\n}\n\nconst DAYS_IN_MONTH = [null, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\nconst DAYS_BEFORE_MONTH = [null];\n\nfor (let dbm = 0, i = 1; i < DAYS_IN_MONTH.length; ++i) {\n    DAYS_BEFORE_MONTH.push(dbm);\n    dbm += DAYS_IN_MONTH[i];\n}\n\nfunction daysInMonth(year, month) {\n    if (month === 2 && isLeap(year)) {\n        return 29;\n    }\n    return DAYS_IN_MONTH[month];\n}\n\nfunction isLeap(year) {\n    return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);\n}\n\nfunction daysBeforeYear(year) {\n    const y = year - 1;\n    return y * 365 + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400);\n}\n\nfunction daysBeforeMonth(year, month) {\n    const postLeapFeb = month > 2 && isLeap(year);\n    return DAYS_BEFORE_MONTH[month] + (postLeapFeb ? 1 : 0);\n}\n\nfunction ymd2ord(year, month, day) {\n    const dim = daysInMonth(year, month);\n    if (!(1 <= day && day <= dim)) {\n        throw new ValueError(`day must be in 1..${dim}`);\n    }\n    return daysBeforeYear(year) + daysBeforeMonth(year, month) + day;\n}\n\nconst DI400Y = daysBeforeYear(401);\nconst DI100Y = daysBeforeYear(101);\nconst DI4Y = daysBeforeYear(5);\n\nfunction ord2ymd(n) {\n    --n;\n    let n400, n100, n4, n1, n0;\n    divmod(n, DI400Y, function (_n400, n) {\n        n400 = _n400;\n        divmod(n, DI100Y, function (_n100, n) {\n            n100 = _n100;\n            divmod(n, DI4Y, function (_n4, n) {\n                n4 = _n4;\n                divmod(n, 365, function (_n1, n) {\n                    n1 = _n1;\n                    n0 = n;\n                });\n            });\n        });\n    });\n\n    n = n0;\n    const year = n400 * 400 + 1 + n100 * 100 + n4 * 4 + n1;\n    if (n1 == 4 || n100 == 100) {\n        assert(n0 === 0);\n        return {\n            year: year - 1,\n            month: 12,\n            day: 31,\n        };\n    }\n\n    const leapyear = n1 === 3 && (n4 !== 24 || n100 == 3);\n    assert(leapyear == isLeap(year));\n    let month = (n + 50) >> 5;\n    let preceding = DAYS_BEFORE_MONTH[month] + (month > 2 && leapyear ? 1 : 0);\n    if (preceding > n) {\n        --month;\n        preceding -= DAYS_IN_MONTH[month] + (month === 2 && leapyear ? 1 : 0);\n    }\n    n -= preceding;\n    return {\n        year: year,\n        month: month,\n        day: n + 1,\n    };\n}\n\n/**\n * Converts the stuff passed in into a valid date, applying overflows as needed\n */\nfunction tmxxx(year, month, day, hour, minute, second, microsecond) {\n    hour = hour || 0;\n    minute = minute || 0;\n    second = second || 0;\n    microsecond = microsecond || 0;\n\n    if (microsecond < 0 || microsecond > 999999) {\n        divmod(microsecond, 1000000, function (carry, ms) {\n            microsecond = ms;\n            second += carry;\n        });\n    }\n    if (second < 0 || second > 59) {\n        divmod(second, 60, function (carry, s) {\n            second = s;\n            minute += carry;\n        });\n    }\n    if (minute < 0 || minute > 59) {\n        divmod(minute, 60, function (carry, m) {\n            minute = m;\n            hour += carry;\n        });\n    }\n    if (hour < 0 || hour > 23) {\n        divmod(hour, 24, function (carry, h) {\n            hour = h;\n            day += carry;\n        });\n    }\n    // That was easy.  Now it gets muddy:  the proper range for day\n    // can't be determined without knowing the correct month and year,\n    // but if day is, e.g., plus or minus a million, the current month\n    // and year values make no sense (and may also be out of bounds\n    // themselves).\n    // Saying 12 months == 1 year should be non-controversial.\n    if (month < 1 || month > 12) {\n        divmod(month - 1, 12, function (carry, m) {\n            month = m + 1;\n            year += carry;\n        });\n    }\n    // Now only day can be out of bounds (year may also be out of bounds\n    // for a datetime object, but we don't care about that here).\n    // If day is out of bounds, what to do is arguable, but at least the\n    // method here is principled and explainable.\n    const dim = daysInMonth(year, month);\n    if (day < 1 || day > dim) {\n        // Move day-1 days from the first of the month.  First try to\n        // get off cheap if we're only one day out of range (adjustments\n        // for timezone alone can't be worse than that).\n        if (day === 0) {\n            --month;\n            if (month > 0) {\n                day = daysInMonth(year, month);\n            } else {\n                --year;\n                month = 12;\n                day = 31;\n            }\n        } else if (day == dim + 1) {\n            ++month;\n            day = 1;\n            if (month > 12) {\n                month = 1;\n                ++year;\n            }\n        } else {\n            const r = ord2ymd(ymd2ord(year, month, 1) + (day - 1));\n            year = r.year;\n            month = r.month;\n            day = r.day;\n        }\n    }\n    return {\n        year: year,\n        month: month,\n        day: day,\n        hour: hour,\n        minute: minute,\n        second: second,\n        microsecond: microsecond,\n    };\n}\n\n// -----------------------------------------------------------------------------\n// Date/Time and related classes\n// -----------------------------------------------------------------------------\n\nexport class PyDate {\n    /**\n     * @returns {PyDate}\n     */\n    static today() {\n        return this.convertDate(new Date());\n    }\n\n    /**\n     * Convert a date object into PyDate\n     * @param {Date} date\n     * @returns {PyDate}\n     */\n    static convertDate(date) {\n        const year = date.getFullYear();\n        const month = date.getMonth() + 1;\n        const day = date.getDate();\n        return new PyDate(year, month, day);\n    }\n\n    /**\n     * @param {integer} year\n     * @param {integer} month\n     * @param {integer} day\n     */\n    constructor(year, month, day) {\n        this.year = year;\n        this.month = month; // 1-indexed => 1 = january, 2 = february, ...\n        this.day = day; // 1-indexed => 1 = first day of month, ...\n    }\n\n    /**\n     * @param  {...any} args\n     * @returns {PyDate}\n     */\n    static create(...args) {\n        const { year, month, day } = parseArgs(args, [\"year\", \"month\", \"day\"]);\n        return new PyDate(year, month, day);\n    }\n\n    /**\n     * @param {PyTimeDelta} timedelta\n     * @returns {PyDate}\n     */\n    add(timedelta) {\n        const s = tmxxx(this.year, this.month, this.day + timedelta.days);\n        return new PyDate(s.year, s.month, s.day);\n    }\n\n    /**\n     * @param {any} other\n     * @returns {boolean}\n     */\n    isEqual(other) {\n        if (!(other instanceof PyDate)) {\n            return false;\n        }\n        return this.year === other.year && this.month === other.month && this.day === other.day;\n    }\n\n    /**\n     * @param {string} format\n     * @returns {string}\n     */\n    strftime(format) {\n        return format.replace(/%([A-Za-z])/g, (m, c) => {\n            switch (c) {\n                case \"Y\":\n                    return fmt4(this.year);\n                case \"m\":\n                    return fmt2(this.month);\n                case \"d\":\n                    return fmt2(this.day);\n            }\n            throw new ValueError(`No known conversion for ${m}`);\n        });\n    }\n\n    /**\n     * @param {PyTimeDelta | PyDate} other\n     * @returns {PyDate | PyTimeDelta}\n     */\n    substract(other) {\n        if (other instanceof PyTimeDelta) {\n            return this.add(other.negate());\n        }\n        if (other instanceof PyDate) {\n            return PyTimeDelta.create(this.toordinal() - other.toordinal());\n        }\n        throw NotSupportedError();\n    }\n\n    /**\n     * @returns {string}\n     */\n    toJSON() {\n        return this.strftime(\"%Y-%m-%d\");\n    }\n\n    /**\n     * @returns {integer}\n     */\n    toordinal() {\n        return ymd2ord(this.year, this.month, this.day);\n    }\n}\n\nexport class PyDateTime {\n    /**\n     * @returns {PyDateTime}\n     */\n    static now() {\n        return this.convertDate(new Date());\n    }\n\n    /**\n     * Convert a date object into PyDateTime\n     * @param {Date} date\n     * @returns {PyDateTime}\n     */\n    static convertDate(date) {\n        const year = date.getFullYear();\n        const month = date.getMonth() + 1;\n        const day = date.getDate();\n        const hour = date.getHours();\n        const minute = date.getMinutes();\n        const second = date.getSeconds();\n        return new PyDateTime(year, month, day, hour, minute, second, 0);\n    }\n\n    /**\n     * @param  {...any} args\n     * @returns {PyDateTime}\n     */\n    static create(...args) {\n        const namedArgs = parseArgs(args, [\n            \"year\",\n            \"month\",\n            \"day\",\n            \"hour\",\n            \"minute\",\n            \"second\",\n            \"microsecond\",\n        ]);\n        const year = namedArgs.year;\n        const month = namedArgs.month;\n        const day = namedArgs.day;\n        const hour = namedArgs.hour || 0;\n        const minute = namedArgs.minute || 0;\n        const second = namedArgs.second || 0;\n        const ms = namedArgs.micro / 1000 || 0;\n        return new PyDateTime(year, month, day, hour, minute, second, ms);\n    }\n\n    /**\n     * @param  {...any} args\n     * @returns {PyDateTime}\n     */\n    static combine(...args) {\n        const { date, time } = parseArgs(args, [\"date\", \"time\"]);\n        // not sure. should we go through constructor instead? what about args normalization?\n        return PyDateTime.create(\n            date.year,\n            date.month,\n            date.day,\n            time.hour,\n            time.minute,\n            time.second\n        );\n    }\n\n    /**\n     * @param {integer} year\n     * @param {integer} month\n     * @param {integer} day\n     * @param {integer} hour\n     * @param {integer} minute\n     * @param {integer} second\n     * @param {integer} microsecond\n     */\n    constructor(year, month, day, hour, minute, second, microsecond) {\n        this.year = year;\n        this.month = month; // 1-indexed => 1 = january, 2 = february, ...\n        this.day = day; // 1-indexed => 1 = first day of month, ...\n        this.hour = hour;\n        this.minute = minute;\n        this.second = second;\n        this.microsecond = microsecond;\n    }\n\n    /**\n     * @param {PyTimeDelta} timedelta\n     * @returns {PyDate}\n     */\n    add(timedelta) {\n        const s = tmxxx(\n            this.year,\n            this.month,\n            this.day + timedelta.days,\n            this.hour,\n            this.minute,\n            this.second + timedelta.seconds,\n            this.microsecond + timedelta.microseconds\n        );\n        // does not seem to closely follow python implementation.\n        return new PyDateTime(s.year, s.month, s.day, s.hour, s.minute, s.second, s.microsecond);\n    }\n\n    /**\n     * @param {any} other\n     * @returns {boolean}\n     */\n    isEqual(other) {\n        if (!(other instanceof PyDateTime)) {\n            return false;\n        }\n        return (\n            this.year === other.year &&\n            this.month === other.month &&\n            this.day === other.day &&\n            this.hour === other.hour &&\n            this.minute === other.minute &&\n            this.second === other.second &&\n            this.microsecond === other.microsecond\n        );\n    }\n\n    /**\n     * @param {string} format\n     * @returns {string}\n     */\n    strftime(format) {\n        return format.replace(/%([A-Za-z])/g, (m, c) => {\n            switch (c) {\n                case \"Y\":\n                    return fmt4(this.year);\n                case \"m\":\n                    return fmt2(this.month);\n                case \"d\":\n                    return fmt2(this.day);\n                case \"H\":\n                    return fmt2(this.hour);\n                case \"M\":\n                    return fmt2(this.minute);\n                case \"S\":\n                    return fmt2(this.second);\n            }\n            throw new ValueError(`No known conversion for ${m}`);\n        });\n    }\n\n    /**\n     * @param {PyTimeDelta} timedelta\n     * @returns {PyDateTime}\n     */\n    substract(timedelta) {\n        return this.add(timedelta.negate());\n    }\n\n    /**\n     * @returns {string}\n     */\n    toJSON() {\n        return this.strftime(\"%Y-%m-%d %H:%M:%S\");\n    }\n\n    /**\n     * @returns {PyDateTime}\n     */\n    to_utc() {\n        const d = new Date(this.year, this.month -1, this.day, this.hour, this.minute, this.second);\n        const timedelta = PyTimeDelta.create({ minutes: d.getTimezoneOffset() });\n        return this.add(timedelta);\n    }\n}\n\nexport class PyTime extends PyDate {\n    /**\n     * @param  {...any} args\n     * @returns {PyTime}\n     */\n    static create(...args) {\n        const namedArgs = parseArgs(args, [\"hour\", \"minute\", \"second\"]);\n        const hour = namedArgs.hour || 0;\n        const minute = namedArgs.minute || 0;\n        const second = namedArgs.second || 0;\n        return new PyTime(hour, minute, second);\n    }\n\n    constructor(hour, minute, second) {\n        const now = new Date();\n        const year = now.getFullYear();\n        const month = now.getMonth();\n        const day = now.getDate();\n        super(year, month, day);\n        this.hour = hour;\n        this.minute = minute;\n        this.second = second;\n    }\n\n    /**\n     * @param {string} format\n     * @returns {string}\n     */\n    strftime(format) {\n        return format.replace(/%([A-Za-z])/g, (m, c) => {\n            switch (c) {\n                case \"Y\":\n                    return fmt4(this.year);\n                case \"m\":\n                    return fmt2(this.month + 1);\n                case \"d\":\n                    return fmt2(this.day);\n                case \"H\":\n                    return fmt2(this.hour);\n                case \"M\":\n                    return fmt2(this.minute);\n                case \"S\":\n                    return fmt2(this.second);\n            }\n            throw new ValueError(`No known conversion for ${m}`);\n        });\n    }\n\n    toJSON() {\n        return this.strftime(\"%H:%M:%S\");\n    }\n}\n\n/*\n * This list is intended to be of that shape (32 days in december), it is used by\n * the algorithm that computes \"relativedelta yearday\". The algorithm was adapted\n * from the one in python (https://github.com/dateutil/dateutil/blob/2.7.3/dateutil/relativedelta.py#L199)\n */\nconst DAYS_IN_YEAR = [31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 366];\n\nconst TIME_PERIODS = [\"hour\", \"minute\", \"second\"];\nconst PERIODS = [\"year\", \"month\", \"day\", ...TIME_PERIODS];\n\nconst RELATIVE_KEYS = \"years months weeks days hours minutes seconds microseconds leapdays\".split(\n    \" \"\n);\nconst ABSOLUTE_KEYS = \"year month day hour minute second microsecond weekday nlyearday yearday\".split(\n    \" \"\n);\n\nconst argsSpec = [\"dt1\", \"dt2\"]; // all other arguments are kwargs\nexport class PyRelativeDelta {\n    /**\n     * @param  {...any} args\n     * @returns {PyRelativeDelta}\n     */\n    static create(...args) {\n        const params = parseArgs(args, argsSpec);\n        if (\"dt1\" in params) {\n            throw new Error(\"relativedelta(dt1, dt2) is not supported for now\");\n        }\n        for (const period of PERIODS) {\n            if (period in params) {\n                const val = params[period];\n                assert(val >= 0, `${period} ${val} is out of range`);\n            }\n        }\n\n        for (const key of RELATIVE_KEYS) {\n            params[key] = params[key] || 0;\n        }\n        for (const key of ABSOLUTE_KEYS) {\n            params[key] = key in params ? params[key] : null;\n        }\n        params.days += 7 * params.weeks;\n\n        let yearDay = 0;\n        if (params.nlyearday) {\n            yearDay = params.nlyearday;\n        } else if (params.yearday) {\n            yearDay = params.yearday;\n            if (yearDay > 59) {\n                params.leapDays = -1;\n            }\n        }\n\n        if (yearDay) {\n            for (let monthIndex = 0; monthIndex < DAYS_IN_YEAR.length; monthIndex++) {\n                if (yearDay <= DAYS_IN_YEAR[monthIndex]) {\n                    params.month = monthIndex + 1;\n                    if (monthIndex === 0) {\n                        params.day = yearDay;\n                    } else {\n                        params.day = yearDay - DAYS_IN_YEAR[monthIndex - 1];\n                    }\n                    break;\n                }\n            }\n        }\n\n        return new PyRelativeDelta(params);\n    }\n\n    /**\n     * @param {PyDateTime|PyDate} date\n     * @param {PyRelativeDelta} delta\n     * @returns {PyDateTime|PyDate}\n     */\n    static add(date, delta) {\n        if (!(date instanceof PyDate || date instanceof PyDateTime)) {\n            throw NotSupportedError();\n        }\n\n        // First pass: we want to determine which is our target year and if we will apply leap days\n        const s = tmxxx(\n            (delta.year || date.year) + delta.years,\n            (delta.month || date.month) + delta.months,\n            delta.day || date.day,\n            delta.hour || date.hour || 0,\n            delta.minute || date.minute || 0,\n            delta.second || date.seconds || 0,\n            delta.microseconds || date.microseconds || 0\n        );\n\n        const newDateTime = new PyDateTime(\n            s.year,\n            s.month,\n            s.day,\n            s.hour,\n            s.minute,\n            s.second,\n            s.microsecond\n        );\n\n        let leapDays = 0;\n        if (delta.leapDays && newDateTime.month > 2 && isLeap(newDateTime.year)) {\n            leapDays = delta.leapDays;\n        }\n\n        // Second pass: apply the difference in days, and the difference in time values\n        const temp = newDateTime.add(\n            PyTimeDelta.create({\n                days: delta.days + leapDays,\n                hours: delta.hours,\n                minutes: delta.minutes,\n                seconds: delta.seconds,\n                microseconds: delta.microseconds,\n            })\n        );\n\n        // Determine the right return type:\n        // First we look at the type of the incoming date object,\n        // then we look at the actual time values held by the computed date.\n        const hasTime = Boolean(temp.hour || temp.minute || temp.second || temp.microsecond);\n        const returnDate =\n            !hasTime && date instanceof PyDate ? new PyDate(temp.year, temp.month, temp.day) : temp;\n\n        // Final pass: target the wanted day of the week (if necessary)\n        if (delta.weekday !== null) {\n            const wantedDow = delta.weekday + 1; // python: Monday is 0 ; JS: Monday is 1;\n            const _date = new Date(returnDate.year, returnDate.month - 1, returnDate.day);\n            const days = (7 - _date.getDay() + wantedDow) % 7;\n            return returnDate.add(new PyTimeDelta(days, 0, 0));\n        }\n        return returnDate;\n    }\n\n    /**\n     * @param {PyDateTime|PyDate} date\n     * @param {PyRelativeDelta} delta\n     * @returns {PyDateTime|PyDate}\n     */\n    static substract(date, delta) {\n        return PyRelativeDelta.add(date, delta.negate());\n    }\n\n    /**\n     * @param {Object} params\n     * @param {+1|-1} sign\n     */\n    constructor(params = {}, sign = +1) {\n        this.years = sign * params.years;\n        this.months = sign * params.months;\n        this.days = sign * params.days;\n        this.hours = sign * params.hours;\n        this.minutes = sign * params.minutes;\n        this.seconds = sign * params.seconds;\n        this.microseconds = sign * params.microseconds;\n\n        this.leapDays = params.leapDays;\n\n        this.year = params.year;\n        this.month = params.month;\n        this.day = params.day;\n        this.hour = params.hour;\n        this.minute = params.minute;\n        this.second = params.second;\n        this.microsecond = params.microsecond;\n\n        this.weekday = params.weekday;\n    }\n\n    /**\n     * @returns {PyRelativeDelta}\n     */\n    negate() {\n        return new PyRelativeDelta(this, -1);\n    }\n\n    isEqual(other) {\n        // For now we don't do normalization in the constructor (or create method).\n        // That is, we only compute the overflows at the time we add or substract.\n        // This is why we can't support isEqual for now.\n        throw new NotSupportedError();\n    }\n}\n\nconst TIME_DELTA_KEYS = \"weeks days hours minutes seconds milliseconds microseconds\".split(\" \");\n\n/**\n * Returns a \"pair\" with the fractional and integer parts of x\n * @param {float}\n * @returns {[float,integer]}\n */\nfunction modf(x) {\n    const mod = x % 1;\n    return [mod < 0 ? mod + 1 : mod, Math.floor(x)];\n}\n\nexport class PyTimeDelta {\n    /**\n     * @param  {...any} args\n     * @returns {PyTimeDelta}\n     */\n    static create(...args) {\n        const namedArgs = parseArgs(args, [\"days\", \"seconds\", \"microseconds\"]);\n        for (const key of TIME_DELTA_KEYS) {\n            namedArgs[key] = namedArgs[key] || 0;\n        }\n\n        // a timedelta can be created using TIME_DELTA_KEYS with float/integer values\n        // but only days, seconds, microseconds are kept internally.\n        // --> some normalization occurs here\n\n        let d = 0;\n        let s = 0;\n        let us = 0; // ~ \u03bcs standard notation for microseconds\n\n        const days = namedArgs.days + namedArgs.weeks * 7;\n        let seconds = namedArgs.seconds + 60 * namedArgs.minutes + 3600 * namedArgs.hours;\n        let microseconds = namedArgs.microseconds + 1000 * namedArgs.milliseconds;\n\n        const [dFrac, dInt] = modf(days);\n        d = dInt;\n        let daysecondsfrac = 0;\n        if (dFrac) {\n            const [dsFrac, dsInt] = modf(dFrac * 24 * 3600);\n            s = dsInt;\n            daysecondsfrac = dsFrac;\n        }\n\n        const [sFrac, sInt] = modf(seconds);\n        seconds = sInt;\n        const secondsfrac = sFrac + daysecondsfrac;\n\n        divmod(seconds, 24 * 3600, (days, seconds) => {\n            d += days;\n            s += seconds;\n        });\n\n        microseconds += secondsfrac * 1e6;\n        divmod(microseconds, 1000000, (seconds, microseconds) => {\n            divmod(seconds, 24 * 3600, (days, seconds) => {\n                d += days;\n                s += seconds;\n                us += Math.round(microseconds);\n            });\n        });\n\n        return new PyTimeDelta(d, s, us);\n    }\n\n    /**\n     * @param {integer} days\n     * @param {integer} seconds\n     * @param {integer} microseconds\n     */\n    constructor(days, seconds, microseconds) {\n        this.days = days;\n        this.seconds = seconds;\n        this.microseconds = microseconds;\n    }\n\n    /**\n     * @param {PyTimeDelta} other\n     * @returns {PyTimeDelta}\n     */\n    add(other) {\n        return PyTimeDelta.create({\n            days: this.days + other.days,\n            seconds: this.seconds + other.seconds,\n            microseconds: this.microseconds + other.microseconds,\n        });\n    }\n\n    /**\n     * @param {integer} n\n     * @returns {PyTimeDelta}\n     */\n    divide(n) {\n        const us = (this.days * 24 * 3600 + this.seconds) * 1e6 + this.microseconds;\n        return PyTimeDelta.create({ microseconds: Math.floor(us / n) });\n    }\n\n    /**\n     * @param {any} other\n     * @returns {boolean}\n     */\n    isEqual(other) {\n        if (!(other instanceof PyTimeDelta)) {\n            return false;\n        }\n        return (\n            this.days === other.days &&\n            this.seconds === other.seconds &&\n            this.microseconds === other.microseconds\n        );\n    }\n\n    /**\n     * @returns {boolean}\n     */\n    isTrue() {\n        return this.days !== 0 || this.seconds !== 0 || this.microseconds !== 0;\n    }\n\n    /**\n     * @param {float} n\n     * @returns {PyTimeDelta}\n     */\n    multiply(n) {\n        return PyTimeDelta.create({\n            days: n * this.days,\n            seconds: n * this.seconds,\n            microseconds: n * this.microseconds,\n        });\n    }\n\n    /**\n     * @returns {PyTimeDelta}\n     */\n    negate() {\n        return PyTimeDelta.create({\n            days: -this.days,\n            seconds: -this.seconds,\n            microseconds: -this.microseconds,\n        });\n    }\n\n    /**\n     * @param {PyTimeDelta} other\n     * @returns {PyTimeDelta}\n     */\n    substract(other) {\n        return PyTimeDelta.create({\n            days: this.days - other.days,\n            seconds: this.seconds - other.seconds,\n            microseconds: this.microseconds - other.microseconds,\n        });\n    }\n\n    /**\n     * @returns {float}\n     */\n    total_seconds() {\n        return this.days * 86400 + this.seconds + this.microseconds / 1000000;\n    }\n}\n", "import { BUILTINS, EvaluationError, execOnIterable } from \"./py_builtin\";\nimport {\n    NotSupportedError,\n    PyDate,\n    PyDateTime,\n    PyRelativeDelta,\n    PyTime,\n    PyTimeDelta,\n} from \"./py_date\";\nimport { PY_DICT, toPyDict } from \"./py_utils\";\nimport { parseArgs } from \"./py_parser\";\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * @typedef { import(\"./py_parser\").AST } AST\n */\n\n// -----------------------------------------------------------------------------\n// Constants and helpers\n// -----------------------------------------------------------------------------\n\nconst isTrue = BUILTINS.bool;\n\n/**\n * @param {AST} ast\n * @param {Object} context\n * @returns {any}\n */\nfunction applyUnaryOp(ast, context) {\n    const value = evaluate(ast.right, context);\n    switch (ast.op) {\n        case \"-\":\n            if (value instanceof Object && value.negate) {\n                return value.negate();\n            }\n            return -value;\n        case \"+\":\n            return value;\n        case \"not\":\n            return !isTrue(value);\n    }\n    throw new EvaluationError(`Unknown unary operator: ${ast.op}`);\n}\n\n/**\n * We want to maintain this order:\n *   None < number (boolean) < dict < string < list < dict\n * So, each type is mapped to a number to represent that order\n *\n * @param {any} val\n * @returns {number} index type\n */\nfunction pytypeIndex(val) {\n    switch (typeof val) {\n        case \"object\":\n            // None, List, Object, Dict\n            return val === null ? 1 : Array.isArray(val) ? 5 : 3;\n        case \"number\":\n            return 2;\n        case \"string\":\n            return 4;\n    }\n    throw new EvaluationError(`Unknown type: ${typeof val}`);\n}\n\n/**\n * @param {Object} obj\n * @returns {boolean}\n */\nfunction isConstructor(obj) {\n    return !!obj.prototype && !!obj.prototype.constructor.name;\n}\n\n/**\n * Compare two values\n *\n * @param {any} left\n * @param {any} right\n * @returns {boolean}\n */\nfunction isLess(left, right) {\n    if (typeof left === \"number\" && typeof right === \"number\") {\n        return left < right;\n    }\n    if (typeof left === \"boolean\") {\n        left = left ? 1 : 0;\n    }\n    if (typeof right === \"boolean\") {\n        right = right ? 1 : 0;\n    }\n    const leftIndex = pytypeIndex(left);\n    const rightIndex = pytypeIndex(right);\n    if (leftIndex === rightIndex) {\n        return left < right;\n    }\n    return leftIndex < rightIndex;\n}\n\n/**\n * @param {any} left\n * @param {any} right\n * @returns {boolean}\n */\nfunction isEqual(left, right) {\n    if (typeof left !== typeof right) {\n        if (typeof left === \"boolean\" && typeof right === \"number\") {\n            return right === (left ? 1 : 0);\n        }\n        if (typeof left === \"number\" && typeof right === \"boolean\") {\n            return left === (right ? 1 : 0);\n        }\n        return false;\n    }\n    if (left instanceof Object && left.isEqual) {\n        return left.isEqual(right);\n    }\n    return left === right;\n}\n\n/**\n * @param {any} left\n * @param {any} right\n * @returns {boolean}\n */\nfunction isIn(left, right) {\n    if (Array.isArray(right)) {\n        return right.includes(left);\n    }\n    if (typeof right === \"string\" && typeof left === \"string\") {\n        return right.includes(left);\n    }\n    if (typeof right === \"object\") {\n        return left in right;\n    }\n    return false;\n}\n\n/**\n * @param {AST} ast\n * @param {object} context\n * @returns {any}\n */\nfunction applyBinaryOp(ast, context) {\n    const left = evaluate(ast.left, context);\n    const right = evaluate(ast.right, context);\n    switch (ast.op) {\n        case \"+\": {\n            const relativeDeltaOnLeft = left instanceof PyRelativeDelta;\n            const relativeDeltaOnRight = right instanceof PyRelativeDelta;\n            if (relativeDeltaOnLeft || relativeDeltaOnRight) {\n                const date = relativeDeltaOnLeft ? right : left;\n                const delta = relativeDeltaOnLeft ? left : right;\n                return PyRelativeDelta.add(date, delta);\n            }\n\n            const timeDeltaOnLeft = left instanceof PyTimeDelta;\n            const timeDeltaOnRight = right instanceof PyTimeDelta;\n            if (timeDeltaOnLeft && timeDeltaOnRight) {\n                return left.add(right);\n            }\n            if (timeDeltaOnLeft) {\n                if (right instanceof PyDate || right instanceof PyDateTime) {\n                    return right.add(left);\n                } else {\n                    throw NotSupportedError();\n                }\n            }\n            if (timeDeltaOnRight) {\n                if (left instanceof PyDate || left instanceof PyDateTime) {\n                    return left.add(right);\n                } else {\n                    throw NotSupportedError();\n                }\n            }\n            if (left instanceof Array && right instanceof Array) {\n                return [...left, ...right];\n            }\n\n            return left + right;\n        }\n        case \"-\": {\n            const isRightDelta = right instanceof PyRelativeDelta;\n            if (isRightDelta) {\n                return PyRelativeDelta.substract(left, right);\n            }\n\n            const timeDeltaOnRight = right instanceof PyTimeDelta;\n            if (timeDeltaOnRight) {\n                if (left instanceof PyTimeDelta) {\n                    return left.substract(right);\n                } else if (left instanceof PyDate || left instanceof PyDateTime) {\n                    return left.substract(right);\n                } else {\n                    throw NotSupportedError();\n                }\n            }\n\n            if (left instanceof PyDate) {\n                return left.substract(right);\n            }\n            return left - right;\n        }\n        case \"*\": {\n            const timeDeltaOnLeft = left instanceof PyTimeDelta;\n            const timeDeltaOnRight = right instanceof PyTimeDelta;\n            if (timeDeltaOnLeft || timeDeltaOnRight) {\n                const number = timeDeltaOnLeft ? right : left;\n                const delta = timeDeltaOnLeft ? left : right;\n                return delta.multiply(number); // check number type?\n            }\n\n            return left * right;\n        }\n        case \"/\":\n            return left / right;\n        case \"%\":\n            return left % right;\n        case \"//\":\n            if (left instanceof PyTimeDelta) {\n                return left.divide(right); // check number type?\n            }\n            return Math.floor(left / right);\n        case \"**\":\n            return left ** right;\n        case \"==\":\n            return isEqual(left, right);\n        case \"<>\":\n        case \"!=\":\n            return !isEqual(left, right);\n        case \"<\":\n            return isLess(left, right);\n        case \">\":\n            return isLess(right, left);\n        case \">=\":\n            return isEqual(left, right) || isLess(right, left);\n        case \"<=\":\n            return isEqual(left, right) || isLess(left, right);\n        case \"in\":\n            return isIn(left, right);\n        case \"not in\":\n            return !isIn(left, right);\n    }\n    throw new EvaluationError(`Unknown binary operator: ${ast.op}`);\n}\n\nconst DICT = {\n    get(...args) {\n        const { key, defValue } = parseArgs(args, [\"key\", \"defValue\"]);\n        if (key in this) {\n            return this[key];\n        } else if (defValue) {\n            return defValue;\n        }\n        return null;\n    },\n};\n\nconst STRING = {\n    lower() {\n        return this.toLowerCase();\n    },\n    upper() {\n        return this.toUpperCase();\n    },\n};\n\nfunction applyFunc(key, func, set, ...args) {\n    // we always receive at least one argument: kwargs (return fnValue(...args, kwargs); in FunctionCall case)\n    if (args.length === 1) {\n        return new Set(set);\n    }\n    if (args.length > 2) {\n        throw new EvaluationError(\n            `${key}: py_js supports at most 1 argument, got (${args.length - 1})`\n        );\n    }\n    return execOnIterable(args[0], func);\n}\n\nconst SET = {\n    intersection(...args) {\n        return applyFunc(\n            \"intersection\",\n            (iterable) => {\n                const intersection = new Set();\n                for (const i of iterable) {\n                    if (this.has(i)) {\n                        intersection.add(i);\n                    }\n                }\n                return intersection;\n            },\n            this,\n            ...args\n        );\n    },\n    difference(...args) {\n        return applyFunc(\n            \"difference\",\n            (iterable) => {\n                iterable = new Set(iterable);\n                const difference = new Set();\n                for (const e of this) {\n                    if (!iterable.has(e)) {\n                        difference.add(e);\n                    }\n                }\n                return difference;\n            },\n            this,\n            ...args\n        );\n    },\n    union(...args) {\n        return applyFunc(\n            \"union\",\n            (iterable) => {\n                return new Set([...this, ...iterable]);\n            },\n            this,\n            ...args\n        );\n    },\n};\n\n// -----------------------------------------------------------------------------\n// Evaluate function\n// -----------------------------------------------------------------------------\n\n/**\n * @param {Function} _class the class whose methods we want\n * @returns {Function[]} an array containing the methods defined on the class,\n *  including the constructor\n */\nfunction methods(_class) {\n    return Object.getOwnPropertyNames(_class.prototype).map((prop) => _class.prototype[prop]);\n}\n\nconst allowedFns = new Set([\n    BUILTINS.time.strftime,\n    BUILTINS.set,\n    BUILTINS.bool,\n    BUILTINS.min,\n    BUILTINS.max,\n    BUILTINS.context_today,\n    BUILTINS.datetime.datetime.now,\n    BUILTINS.datetime.datetime.combine,\n    BUILTINS.datetime.date.today,\n    ...methods(BUILTINS.relativedelta),\n    ...Object.values(BUILTINS.datetime).flatMap((obj) => methods(obj)),\n    ...Object.values(SET),\n    ...Object.values(DICT),\n    ...Object.values(STRING),\n]);\n\nconst unboundFn = Symbol(\"unbound function\");\n\n/**\n * @param {AST} ast\n * @param {Object} context\n * @returns {any}\n */\nexport function evaluate(ast, context = {}) {\n    const dicts = new Set();\n    let pyContext;\n    const evalContext = Object.create(context);\n    if (!evalContext.context) {\n        Object.defineProperty(evalContext, \"context\", {\n            get() {\n                if (!pyContext) {\n                    pyContext = toPyDict(context);\n                }\n                return pyContext;\n            },\n        });\n    }\n\n    function _innerEvaluate(ast) {\n        switch (ast.type) {\n            case 0 /* Number */:\n            case 1 /* String */:\n                return ast.value;\n            case 5 /* Name */:\n                if (ast.value in evalContext) {\n                    return evalContext[ast.value];\n                } else if (ast.value in BUILTINS) {\n                    return BUILTINS[ast.value];\n                } else {\n                    throw new EvaluationError(`Name '${ast.value}' is not defined`);\n                }\n            case 3 /* None */:\n                return null;\n            case 2 /* Boolean */:\n                return ast.value;\n            case 6 /* UnaryOperator */:\n                return applyUnaryOp(ast, evalContext);\n            case 7 /* BinaryOperator */:\n                return applyBinaryOp(ast, evalContext);\n            case 14 /* BooleanOperator */: {\n                const left = _evaluate(ast.left);\n                if (ast.op === \"and\") {\n                    return isTrue(left) ? _evaluate(ast.right) : left;\n                } else {\n                    return isTrue(left) ? left : _evaluate(ast.right);\n                }\n            }\n            case 4 /* List */:\n            case 10 /* Tuple */:\n                return ast.value.map(_evaluate);\n            case 11 /* Dictionary */: {\n                const dict = {};\n                for (const key in ast.value) {\n                    dict[key] = _evaluate(ast.value[key]);\n                }\n                dicts.add(dict);\n                return dict;\n            }\n            case 8 /* FunctionCall */: {\n                const fnValue = _evaluate(ast.fn);\n                const args = ast.args.map(_evaluate);\n                const kwargs = {};\n                for (const kwarg in ast.kwargs) {\n                    kwargs[kwarg] = _evaluate(ast.kwargs[kwarg]);\n                }\n                if (\n                    fnValue === PyDate ||\n                    fnValue === PyDateTime ||\n                    fnValue === PyTime ||\n                    fnValue === PyRelativeDelta ||\n                    fnValue === PyTimeDelta\n                ) {\n                    return fnValue.create(...args, kwargs);\n                }\n                return fnValue(...args, kwargs);\n            }\n            case 12 /* Lookup */: {\n                const dict = _evaluate(ast.target);\n                const key = _evaluate(ast.key);\n                return dict[key];\n            }\n            case 13 /* If */: {\n                if (isTrue(_evaluate(ast.condition))) {\n                    return _evaluate(ast.ifTrue);\n                } else {\n                    return _evaluate(ast.ifFalse);\n                }\n            }\n            case 15 /* ObjLookup */: {\n                let left = _evaluate(ast.obj);\n                let result;\n                if (dicts.has(left) || Object.isPrototypeOf.call(PY_DICT, left)) {\n                    // this is a dictionary => need to apply dict methods\n                    result = DICT[ast.key];\n                } else if (typeof left === \"string\") {\n                    result = STRING[ast.key];\n                } else if (left instanceof Set) {\n                    result = SET[ast.key];\n                } else if (ast.key == \"get\" && typeof left === \"object\") {\n                    result = DICT[ast.key];\n                    left = toPyDict(left);\n                } else {\n                    result = left[ast.key];\n                }\n                if (typeof result === \"function\") {\n                    if (!isConstructor(result)) {\n                        const bound = result.bind(left);\n                        bound[unboundFn] = result;\n                        return bound;\n                    }\n                }\n                return result;\n            }\n        }\n        throw new EvaluationError(`AST of type ${ast.type} cannot be evaluated`);\n    }\n\n    /**\n     * @param {AST} ast\n     */\n    function _evaluate(ast) {\n        const val = _innerEvaluate(ast);\n        if (typeof val === \"function\" && !allowedFns.has(val) && !allowedFns.has(val[unboundFn])) {\n            throw new Error(\"Invalid Function Call\");\n        }\n        return val;\n    }\n    return _evaluate(ast);\n}\n", "import { binaryOperators, comparators } from \"./py_tokenizer\";\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * @typedef { import(\"./py_tokenizer\").Token } Token\n */\n\n/**\n * @typedef {{type: 0, value: number}} ASTNumber\n * @typedef {{type: 1, value: string}} ASTString\n * @typedef {{type: 2, value: boolean}} ASTBoolean\n * @typedef {{type: 3}} ASTNone\n * @typedef {{type: 4, value: AST[]}} ASTList\n * @typedef {{type: 5, value: string}} ASTName\n * @typedef {{type: 6, op: string, right: AST}} ASTUnaryOperator\n * @typedef {{type: 7, op: string, left: AST, right: AST}} ASTBinaryOperator\n * @typedef {{type: 8, fn: AST, args: AST[], kwargs: {[key: string]: AST}}} ASTFunctionCall\n * @typedef {{type: 9, name: ASTName, value: AST}} ASTAssignment\n * @typedef {{type: 10, value: AST[]}} ASTTuple\n * @typedef {{type: 11, value: { [key: string]: AST}}} ASTDictionary\n * @typedef {{type: 12, target: AST, key: AST}} ASTLookup\n * @typedef {{type: 13, condition: AST, ifTrue: AST, ifFalse: AST}} ASTIf\n * @typedef {{type: 14, op: string, left: AST, right: AST}} ASTBooleanOperator\n * @typedef {{type: 15, obj: AST, key: string}} ASTObjLookup\n *\n * @typedef { ASTNumber | ASTString | ASTBoolean | ASTNone | ASTList | ASTName | ASTUnaryOperator | ASTBinaryOperator | ASTFunctionCall | ASTAssignment | ASTTuple | ASTDictionary |ASTLookup | ASTIf | ASTBooleanOperator | ASTObjLookup} AST\n */\n\nexport class ParserError extends Error {}\n\n// -----------------------------------------------------------------------------\n// Constants and helpers\n// -----------------------------------------------------------------------------\n\nconst chainedOperators = new Set(comparators);\nconst infixOperators = new Set(binaryOperators.concat(comparators));\n\n/**\n * Compute the \"binding power\" of a symbol\n *\n * @param {string} symbol\n * @returns {number}\n */\nexport function bp(symbol) {\n    switch (symbol) {\n        case \"=\":\n            return 10;\n        case \"if\":\n            return 20;\n        case \"in\":\n        case \"not in\":\n        case \"is\":\n        case \"is not\":\n        case \"<\":\n        case \"<=\":\n        case \">\":\n        case \">=\":\n        case \"<>\":\n        case \"==\":\n        case \"!=\":\n            return 60;\n        case \"or\":\n            return 30;\n        case \"and\":\n            return 40;\n        case \"not\":\n            return 50;\n        case \"|\":\n            return 70;\n        case \"^\":\n            return 80;\n        case \"&\":\n            return 90;\n        case \"<<\":\n        case \">>\":\n            return 100;\n        case \"+\":\n        case \"-\":\n            return 110;\n        case \"*\":\n        case \"/\":\n        case \"//\":\n        case \"%\":\n            return 120;\n        case \"**\":\n            return 140;\n        case \".\":\n        case \"(\":\n        case \"[\":\n            return 150;\n    }\n    return 0;\n}\n\n/**\n * Compute binding power of a symbol\n *\n * @param {Token} token\n * @returns {number}\n */\nfunction bindingPower(token) {\n    return token.type === 2 /* Symbol */ ? bp(token.value) : 0;\n}\n\n/**\n * Check if a token is a symbol of a given value\n *\n * @param {Token} token\n * @param {string} value\n * @returns {boolean}\n */\nfunction isSymbol(token, value) {\n    return token.type === 2 /* Symbol */ && token.value === value;\n}\n\n/**\n * @param {Token} current\n * @param {Token[]} tokens\n * @returns {AST}\n */\nfunction parsePrefix(current, tokens) {\n    switch (current.type) {\n        case 0 /* Number */:\n            return { type: 0 /* Number */, value: current.value };\n        case 1 /* String */:\n            return { type: 1 /* String */, value: current.value };\n        case 4 /* Constant */:\n            if (current.value === \"None\") {\n                return { type: 3 /* None */ };\n            } else {\n                return { type: 2 /* Boolean */, value: current.value === \"True\" };\n            }\n        case 3 /* Name */:\n            return { type: 5 /* Name */, value: current.value };\n        case 2 /* Symbol */:\n            switch (current.value) {\n                case \"-\":\n                case \"+\":\n                case \"~\":\n                    return {\n                        type: 6 /* UnaryOperator */,\n                        op: current.value,\n                        right: _parse(tokens, 130),\n                    };\n                case \"not\":\n                    return {\n                        type: 6 /* UnaryOperator */,\n                        op: current.value,\n                        right: _parse(tokens, 50),\n                    };\n                case \"(\": {\n                    const content = [];\n                    let isTuple = false;\n                    while (tokens[0] && !isSymbol(tokens[0], \")\")) {\n                        content.push(_parse(tokens, 0));\n                        if (tokens[0]) {\n                            if (tokens[0] && isSymbol(tokens[0], \",\")) {\n                                isTuple = true;\n                                tokens.shift();\n                            } else if (!isSymbol(tokens[0], \")\")) {\n                                throw new ParserError(\"parsing error\");\n                            }\n                        } else {\n                            throw new ParserError(\"parsing error\");\n                        }\n                    }\n                    if (!tokens[0] || !isSymbol(tokens[0], \")\")) {\n                        throw new ParserError(\"parsing error\");\n                    }\n                    tokens.shift();\n                    isTuple = isTuple || content.length === 0;\n                    return isTuple ? { type: 10 /* Tuple */, value: content } : content[0];\n                }\n                case \"[\": {\n                    const value = [];\n                    while (tokens[0] && !isSymbol(tokens[0], \"]\")) {\n                        value.push(_parse(tokens, 0));\n                        if (tokens[0]) {\n                            if (isSymbol(tokens[0], \",\")) {\n                                tokens.shift();\n                            } else if (!isSymbol(tokens[0], \"]\")) {\n                                throw new ParserError(\"parsing error\");\n                            }\n                        }\n                    }\n                    if (!tokens[0] || !isSymbol(tokens[0], \"]\")) {\n                        throw new ParserError(\"parsing error\");\n                    }\n                    tokens.shift();\n                    return { type: 4 /* List */, value };\n                }\n                case \"{\": {\n                    const dict = {};\n                    while (tokens[0] && !isSymbol(tokens[0], \"}\")) {\n                        const key = _parse(tokens, 0);\n                        if (\n                            (key.type !== 1 /* String */ && key.type !== 0) /* Number */ ||\n                            !tokens[0] ||\n                            !isSymbol(tokens[0], \":\")\n                        ) {\n                            throw new ParserError(\"parsing error\");\n                        }\n                        tokens.shift();\n                        const value = _parse(tokens, 0);\n                        dict[key.value] = value;\n                        if (isSymbol(tokens[0], \",\")) {\n                            tokens.shift();\n                        }\n                    }\n                    // remove the } token\n                    if (!tokens.shift()) {\n                        throw new ParserError(\"parsing error\");\n                    }\n                    return { type: 11 /* Dictionary */, value: dict };\n                }\n            }\n    }\n    throw new ParserError(\"Token cannot be parsed\");\n}\n\n/**\n * @param {AST} ast\n * @param {Token} current\n * @param {Token[]} tokens\n * @returns {AST}\n */\nfunction parseInfix(left, current, tokens) {\n    switch (current.type) {\n        case 2 /* Symbol */:\n            if (infixOperators.has(current.value)) {\n                let right = _parse(tokens, bindingPower(current));\n                if (current.value === \"and\" || current.value === \"or\") {\n                    return {\n                        type: 14 /* BooleanOperator */,\n                        op: current.value,\n                        left,\n                        right,\n                    };\n                } else if (current.value === \".\") {\n                    if (right.type === 5 /* Name */) {\n                        return {\n                            type: 15 /* ObjLookup */,\n                            obj: left,\n                            key: right.value,\n                        };\n                    } else {\n                        throw new ParserError(\"invalid obj lookup\");\n                    }\n                }\n                let op = {\n                    type: 7 /* BinaryOperator */,\n                    op: current.value,\n                    left,\n                    right,\n                };\n                while (\n                    chainedOperators.has(current.value) &&\n                    tokens[0] &&\n                    tokens[0].type === 2 /* Symbol */ &&\n                    chainedOperators.has(tokens[0].value)\n                ) {\n                    const nextToken = tokens.shift();\n                    op = {\n                        type: 14 /* BooleanOperator */,\n                        op: \"and\",\n                        left: op,\n                        right: {\n                            type: 7 /* BinaryOperator */,\n                            op: nextToken.value,\n                            left: right,\n                            right: _parse(tokens, bindingPower(nextToken)),\n                        },\n                    };\n                    right = op.right.right;\n                }\n                return op;\n            }\n            switch (current.value) {\n                case \"(\": {\n                    // function call\n                    const args = [];\n                    const kwargs = {};\n                    while (tokens[0] && !isSymbol(tokens[0], \")\")) {\n                        const arg = _parse(tokens, 0);\n                        if (arg.type === 9 /* Assignment */) {\n                            kwargs[arg.name.value] = arg.value;\n                        } else {\n                            args.push(arg);\n                        }\n                        if (tokens[0] && isSymbol(tokens[0], \",\")) {\n                            tokens.shift();\n                        }\n                    }\n                    if (!tokens[0] || !isSymbol(tokens[0], \")\")) {\n                        throw new ParserError(\"parsing error\");\n                    }\n                    tokens.shift();\n                    return { type: 8 /* FunctionCall */, fn: left, args, kwargs };\n                }\n                case \"=\":\n                    if (left.type === 5 /* Name */) {\n                        return {\n                            type: 9 /* Assignment */,\n                            name: left,\n                            value: _parse(tokens, 10),\n                        };\n                    }\n                    break;\n                case \"[\": {\n                    // lookup in dictionary\n                    const key = _parse(tokens);\n                    if (!tokens[0] || !isSymbol(tokens[0], \"]\")) {\n                        throw new ParserError(\"parsing error\");\n                    }\n                    tokens.shift();\n                    return {\n                        type: 12 /* Lookup */,\n                        target: left,\n                        key: key,\n                    };\n                }\n                case \"if\": {\n                    const condition = _parse(tokens);\n                    if (!tokens[0] || !isSymbol(tokens[0], \"else\")) {\n                        throw new ParserError(\"parsing error\");\n                    }\n                    tokens.shift();\n                    const ifFalse = _parse(tokens);\n                    return {\n                        type: 13 /* If */,\n                        condition,\n                        ifTrue: left,\n                        ifFalse,\n                    };\n                }\n            }\n    }\n    throw new ParserError(\"Token cannot be parsed\");\n}\n\n/**\n * @param {Token[]} tokens\n * @param {number} [bp]\n * @returns {AST}\n */\nfunction _parse(tokens, bp = 0) {\n    const token = tokens.shift();\n    let expr = parsePrefix(token, tokens);\n    while (tokens[0] && bindingPower(tokens[0]) > bp) {\n        expr = parseInfix(expr, tokens.shift(), tokens);\n    }\n    return expr;\n}\n\n// -----------------------------------------------------------------------------\n// Parse function\n// -----------------------------------------------------------------------------\n\n/**\n * Parse a list of tokens\n *\n * @param {Token[]} tokens\n * @returns {AST}\n */\nexport function parse(tokens) {\n    if (tokens.length) {\n        const ast = _parse(tokens, 0);\n        if (tokens.length) {\n            throw new ParserError(\"Token(s) unused\");\n        }\n        return ast;\n    }\n    throw new ParserError(\"Missing token\");\n}\n\n/**\n * @param {any[]} args\n * @param {string[]} spec\n * @returns {{[name: string]: any}}\n */\nexport function parseArgs(args, spec) {\n    const last = args[args.length - 1];\n    const unnamedArgs = typeof last === \"object\" ? args.slice(0, -1) : args;\n    const kwargs = typeof last === \"object\" ? last : {};\n    for (const [index, val] of unnamedArgs.entries()) {\n        kwargs[spec[index]] = val;\n    }\n    return kwargs;\n}\n", "// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * @typedef {{type: 0, value: number}} TokenNumber\n *\n * @typedef {{type: 1, value: string}} TokenString\n *\n * @typedef {{type: 2, value: string}} TokenSymbol\n *\n * @typedef {{type: 3, value: string}} TokenName\n *\n * @typedef {{type: 4, value: string}} TokenConstant\n *\n * @typedef {TokenNumber | TokenString | TokenSymbol | TokenName | TokenConstant} Token\n */\n\nexport class TokenizerError extends Error {}\n\n// -----------------------------------------------------------------------------\n// Helpers and Constants\n// -----------------------------------------------------------------------------\n\n/**\n * Directly maps a single escape code to an output character\n */\nconst directMap = {\n    \"\\\\\": \"\\\\\",\n    '\"': '\"',\n    \"'\": \"'\",\n    a: \"\\x07\",\n    b: \"\\x08\",\n    f: \"\\x0c\",\n    n: \"\\n\",\n    r: \"\\r\",\n    t: \"\\t\",\n    v: \"\\v\",\n};\n\n/**\n * Implements the decoding of Python string literals (embedded in\n * JS strings) into actual JS strings. This includes the decoding\n * of escapes into their corresponding JS\n * characters/codepoints/whatever.\n *\n * The ``unicode`` flags notes whether the literal should be\n * decoded as a bytestring literal or a unicode literal, which\n * pretty much only impacts decoding (or not) of unicode escapes\n * at this point since bytestrings are not technically handled\n * (everything is decoded to JS \"unicode\" strings)\n *\n * Eventurally, ``str`` could eventually use typed arrays, that'd\n * be interesting...\n *\n * @param {string} str\n * @param {boolean} unicode\n * @returns {string}\n */\nfunction decodeStringLiteral(str, unicode) {\n    const out = [];\n    let code;\n    for (var i = 0; i < str.length; ++i) {\n        if (str[i] !== \"\\\\\") {\n            out.push(str[i]);\n            continue;\n        }\n        var escape = str[i + 1];\n        if (escape in directMap) {\n            out.push(directMap[escape]);\n            ++i;\n            continue;\n        }\n        switch (escape) {\n            // Ignored\n            case \"\\n\":\n                ++i;\n                continue;\n            // Character named name in the Unicode database (Unicode only)\n            case \"N\":\n                if (!unicode) {\n                    break;\n                }\n                throw new TokenizerError(\"SyntaxError: \\\\N{} escape not implemented\");\n            case \"u\":\n                if (!unicode) {\n                    break;\n                }\n                var uni = str.slice(i + 2, i + 6);\n                if (!/[0-9a-f]{4}/i.test(uni)) {\n                    throw new TokenizerError(\n                        [\n                            \"SyntaxError: (unicode error) 'unicodeescape' codec\",\n                            \" can't decode bytes in position \",\n                            i,\n                            \"-\",\n                            i + 4,\n                            \": truncated \\\\uXXXX escape\",\n                        ].join(\"\")\n                    );\n                }\n                code = parseInt(uni, 16);\n                out.push(String.fromCharCode(code));\n                // escape + 4 hex digits\n                i += 5;\n                continue;\n            case \"U\":\n                if (!unicode) {\n                    break;\n                }\n                // TODO: String.fromCodePoint\n                throw new TokenizerError(\"SyntaxError: \\\\U escape not implemented\");\n            case \"x\":\n                // get 2 hex digits\n                var hex = str.slice(i + 2, i + 4);\n                if (!/[0-9a-f]{2}/i.test(hex)) {\n                    if (!unicode) {\n                        throw new TokenizerError(\"ValueError: invalid \\\\x escape\");\n                    }\n                    throw new TokenizerError(\n                        [\n                            \"SyntaxError: (unicode error) 'unicodeescape'\",\n                            \" codec can't decode bytes in position \",\n                            i,\n                            \"-\",\n                            i + 2,\n                            \": truncated \\\\xXX escape\",\n                        ].join(\"\")\n                    );\n                }\n                code = parseInt(hex, 16);\n                out.push(String.fromCharCode(code));\n                // skip escape + 2 hex digits\n                i += 3;\n                continue;\n            default:\n                // Check if octal\n                if (!/[0-8]/.test(escape)) {\n                    break;\n                }\n                var r = /[0-8]{1,3}/g;\n                r.lastIndex = i + 1;\n                var m = r.exec(str);\n                var oct = m[0];\n                code = parseInt(oct, 8);\n                out.push(String.fromCharCode(code));\n                // skip matchlength\n                i += oct.length;\n                continue;\n        }\n        out.push(\"\\\\\");\n    }\n    return out.join(\"\");\n}\n\nconst constants = new Set([\"None\", \"False\", \"True\"]);\n\nexport const comparators = [\n    \"in\",\n    \"not\",\n    \"not in\",\n    \"is\",\n    \"is not\",\n    \"<\",\n    \"<=\",\n    \">\",\n    \">=\",\n    \"<>\",\n    \"!=\",\n    \"==\",\n];\n\nexport const binaryOperators = [\n    \"or\",\n    \"and\",\n    \"|\",\n    \"^\",\n    \"&\",\n    \"<<\",\n    \">>\",\n    \"+\",\n    \"-\",\n    \"*\",\n    \"/\",\n    \"//\",\n    \"%\",\n    \"~\",\n    \"**\",\n    \".\",\n];\n\nexport const unaryOperators = [\"-\"];\n\nconst symbols = new Set([\n    ...[\"(\", \")\", \"[\", \"]\", \"{\", \"}\", \":\", \",\"],\n    ...[\"if\", \"else\", \"lambda\", \"=\"],\n    ...comparators,\n    ...binaryOperators,\n    ...unaryOperators,\n]);\n\n// Regexps\nfunction group(...args) {\n    return \"(\" + args.join(\"|\") + \")\";\n}\n\nconst Name = \"[a-zA-Z_]\\\\w*\";\nconst Whitespace = \"[ \\\\f\\\\t]*\";\nconst DecNumber = \"\\\\d+(L|l)?\";\nconst IntNumber = DecNumber;\n\nconst Exponent = \"[eE][+-]?\\\\d+\";\nconst PointFloat = group(`\\\\d+\\\\.\\\\d*(${Exponent})?`, `\\\\.\\\\d+(${Exponent})?`);\n// Exponent not optional when no decimal point\nconst FloatNumber = group(PointFloat, `\\\\d+${Exponent}`);\n\nconst Number = group(FloatNumber, IntNumber);\nconst Operator = group(\"\\\\*\\\\*=?\", \">>=?\", \"<<=?\", \"<>\", \"!=\", \"//=?\", \"[+\\\\-*/%&|^=<>]=?\", \"~\");\nconst Bracket = \"[\\\\[\\\\]\\\\(\\\\)\\\\{\\\\}]\";\nconst Special = \"[:;.,`@]\";\nconst Funny = group(Operator, Bracket, Special);\nconst ContStr = group(\n    \"([uU])?'([^\\n'\\\\\\\\]*(?:\\\\\\\\.[^\\n'\\\\\\\\]*)*)'\",\n    '([uU])?\"([^\\n\"\\\\\\\\]*(?:\\\\\\\\.[^\\n\"\\\\\\\\]*)*)\"'\n);\nconst PseudoToken = Whitespace + group(Number, Funny, ContStr, Name);\nconst NumberPattern = new RegExp(\"^\" + Number + \"$\");\nconst StringPattern = new RegExp(\"^\" + ContStr + \"$\");\nconst NamePattern = new RegExp(\"^\" + Name + \"$\");\nconst strip = new RegExp(\"^\" + Whitespace);\n\n// -----------------------------------------------------------------------------\n// Tokenize function\n// -----------------------------------------------------------------------------\n\n/**\n * Transform a string into a list of tokens\n *\n * @param {string} str\n * @returns {Token[]}\n */\nexport function tokenize(str) {\n    const tokens = [];\n    const max = str.length;\n    let start = 0;\n    let end = 0;\n    // /g flag makes repeated exec() have memory\n    const pseudoprog = new RegExp(PseudoToken, \"g\");\n    while (pseudoprog.lastIndex < max) {\n        const pseudomatch = pseudoprog.exec(str);\n        if (!pseudomatch) {\n            // if match failed on trailing whitespace, end tokenizing\n            if (/^\\s+$/.test(str.slice(end))) {\n                break;\n            }\n            throw new TokenizerError(\n                \"Failed to tokenize <<\" +\n                    str +\n                    \">> at index \" +\n                    (end || 0) +\n                    \"; parsed so far: \" +\n                    tokens\n            );\n        }\n        if (pseudomatch.index > end) {\n            if (str.slice(end, pseudomatch.index).trim()) {\n                throw new TokenizerError(\"Invalid expression\");\n            }\n        }\n        start = pseudomatch.index;\n        end = pseudoprog.lastIndex;\n        let token = str.slice(start, end).replace(strip, \"\");\n        if (NumberPattern.test(token)) {\n            tokens.push({\n                type: 0 /* Number */,\n                value: parseFloat(token),\n            });\n        } else if (StringPattern.test(token)) {\n            var m = StringPattern.exec(token);\n            tokens.push({\n                type: 1 /* String */,\n                value: decodeStringLiteral(m[3] !== undefined ? m[3] : m[5], !!(m[2] || m[4])),\n            });\n        } else if (symbols.has(token)) {\n            // transform 'not in' and 'is not' in a single token\n            if (token === \"in\" && tokens.length > 0 && tokens[tokens.length - 1].value === \"not\") {\n                token = \"not in\";\n                tokens.pop();\n            } else if (\n                token === \"not\" &&\n                tokens.length > 0 &&\n                tokens[tokens.length - 1].value === \"is\"\n            ) {\n                token = \"is not\";\n                tokens.pop();\n            }\n            tokens.push({\n                type: 2 /* Symbol */,\n                value: token,\n            });\n        } else if (constants.has(token)) {\n            tokens.push({\n                type: 4 /* Constant */,\n                value: token,\n            });\n        } else if (NamePattern.test(token)) {\n            tokens.push({\n                type: 3 /* Name */,\n                value: token,\n            });\n        } else {\n            throw new TokenizerError(\"Invalid expression\");\n        }\n    }\n    return tokens;\n}\n", "import { bp } from \"./py_parser\";\nimport { PyDate, PyDateTime } from \"./py_date\";\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * @typedef { import(\"./py_parser\").AST } AST\n */\n\n// -----------------------------------------------------------------------------\n// Utils\n// -----------------------------------------------------------------------------\n\n/**\n * Represent any value as a primitive AST\n *\n * @param {any} value\n * @returns {AST}\n */\nexport function toPyValue(value) {\n    switch (typeof value) {\n        case \"string\":\n            return { type: 1 /* String */, value };\n        case \"number\":\n            return { type: 0 /* Number */, value };\n        case \"boolean\":\n            return { type: 2 /* Boolean */, value };\n        case \"object\":\n            if (Array.isArray(value)) {\n                return { type: 4 /* List */, value: value.map(toPyValue) };\n            } else if (value === null) {\n                return { type: 3 /* None */ };\n            } else if (value instanceof Date) {\n                return { type: 1, value: PyDateTime.convertDate(value) };\n            } else if (value instanceof PyDate || value instanceof PyDateTime) {\n                return { type: 1, value };\n            } else {\n                const content = {};\n                for (const key in value) {\n                    content[key] = toPyValue(value[key]);\n                }\n                return { type: 11 /* Dictionary */, value: content };\n            }\n        default:\n            throw new Error(\"Invalid type\");\n    }\n}\n\n/**\n * @param {AST} ast\n * @param {number} [lbp] left binding power\n * @return {string}\n */\nexport function formatAST(ast, lbp = 0) {\n    switch (ast.type) {\n        case 3 /* None */:\n            return \"None\";\n        case 1 /* String */:\n            return JSON.stringify(ast.value);\n        case 0 /* Number */:\n            return String(ast.value);\n        case 2 /* Boolean */:\n            return ast.value ? \"True\" : \"False\";\n        case 4 /* List */:\n            return `[${ast.value.map(formatAST).join(\", \")}]`;\n        case 6 /* UnaryOperator */:\n            if (ast.op === \"not\") {\n                return `not ` + formatAST(ast.right, 50);\n            }\n            return ast.op + formatAST(ast.right, 130);\n        case 7 /* BinaryOperator */: {\n            const abp = bp(ast.op);\n            const str = `${formatAST(ast.left, abp)} ${ast.op} ${formatAST(ast.right, abp)}`;\n            return abp < lbp ? `(${str})` : str;\n        }\n        case 11 /* Dictionary */: {\n            const pairs = [];\n            for (const k in ast.value) {\n                pairs.push(`\"${k}\": ${formatAST(ast.value[k])}`);\n            }\n            return `{` + pairs.join(\", \") + `}`;\n        }\n        case 10 /* Tuple */:\n            return `(${ast.value.map(formatAST).join(\", \")})`;\n        case 5 /* Name */:\n            return ast.value;\n        case 12 /* Lookup */: {\n            return `${formatAST(ast.target)}[${formatAST(ast.key)}]`;\n        }\n        case 13 /* If */: {\n            const { ifTrue, condition, ifFalse } = ast;\n            return `${formatAST(ifTrue)} if ${formatAST(condition)} else ${formatAST(ifFalse)}`;\n        }\n        case 14 /* BooleanOperator */: {\n            const abp = bp(ast.op);\n            const str = `${formatAST(ast.left, abp)} ${ast.op} ${formatAST(ast.right, abp)}`;\n            return abp < lbp ? `(${str})` : str;\n        }\n        case 15 /* ObjLookup */:\n            return `${formatAST(ast.obj, 150)}.${ast.key}`;\n        case 8 /* FunctionCall */: {\n            const args = ast.args.map(formatAST);\n            const kwargs = [];\n            for (const kwarg in ast.kwargs) {\n                kwargs.push(`${kwarg} = ${formatAST(ast.kwargs[kwarg])}`);\n            }\n            const argStr = args.concat(kwargs).join(\", \");\n            return `${formatAST(ast.fn)}(${argStr})`;\n        }\n    }\n    throw new Error(\"invalid expression: \" + ast);\n}\n\nexport const PY_DICT = Object.create(null);\n\n/**\n * @param {Object} obj\n * @returns {AST} a python dictionary\n */\nexport function toPyDict(obj) {\n    return new Proxy(obj, {\n        getPrototypeOf() {\n            return PY_DICT;\n        },\n    });\n}\n", "import { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { RecordAutocomplete } from \"./record_autocomplete\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useTagNavigation } from \"./tag_navigation_hook\";\n\nexport class MultiRecordSelector extends Component {\n    static props = {\n        resIds: { type: Array, element: Number },\n        resModel: String,\n        update: Function,\n        domain: { type: Array, optional: true },\n        context: { type: Object, optional: true },\n        fieldString: { type: String, optional: true },\n        placeholder: { type: String, optional: true },\n    };\n    static components = { RecordAutocomplete, TagsList };\n    static template = \"web.MultiRecordSelector\";\n\n    setup() {\n        this.nameService = useService(\"name\");\n        this.onTagKeydown = useTagNavigation(\"multiRecordSelector\", this.deleteTag.bind(this));\n        onWillStart(() => this.computeDerivedParams());\n        onWillUpdateProps((nextProps) => this.computeDerivedParams(nextProps));\n    }\n\n    async computeDerivedParams(props = this.props) {\n        const displayNames = await this.getDisplayNames(props);\n        this.tags = this.getTags(props, displayNames);\n    }\n\n    async getDisplayNames(props) {\n        const ids = this.getIds(props);\n        return this.nameService.loadDisplayNames(props.resModel, ids);\n    }\n\n    /**\n     * Placeholder should be empty if there is at least one tag. We cannot use\n     * the default behavior of the input placeholder because even if there is\n     * a tag, the input is still empty.\n     */\n    get placeholder() {\n        return this.getIds().length ? \"\" : this.props.placeholder;\n    }\n\n    getIds(props = this.props) {\n        return props.resIds;\n    }\n\n    getTags(props, displayNames) {\n        return props.resIds.map((id, index) => {\n            const text =\n                typeof displayNames[id] === \"string\"\n                    ? displayNames[id]\n                    : _t(\"Inaccessible/missing record ID: %s\", id);\n            return {\n                text,\n                onDelete: () => {\n                    this.deleteTag(index);\n                },\n                onKeydown: this.onTagKeydown,\n            };\n        });\n    }\n\n    deleteTag(index) {\n        this.props.update([\n            ...this.props.resIds.slice(0, index),\n            ...this.props.resIds.slice(index + 1),\n        ]);\n    }\n\n    update(resIds) {\n        this.props.update([...this.props.resIds, ...resIds]);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Domain } from \"@web/core/domain\";\nimport { registry } from \"@web/core/registry\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\n\nconst SEARCH_LIMIT = 7;\nconst SEARCH_MORE_LIMIT = 320;\n\nexport class RecordAutocomplete extends Component {\n    static props = {\n        resModel: String,\n        update: Function,\n        multiSelect: Boolean,\n        getIds: Function,\n        value: String,\n        domain: { type: Array, optional: true },\n        context: { type: Object, optional: true },\n        className: { type: String, optional: true },\n        fieldString: { type: String, optional: true },\n        placeholder: { type: String, optional: true },\n    };\n    static components = { AutoComplete };\n    static template = \"web.RecordAutocomplete\";\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.nameService = useService(\"name\");\n        this.addDialog = useOwnedDialogs();\n        this.sources = [\n            {\n                placeholder: _t(\"Loading...\"),\n                options: this.loadOptionsSource.bind(this),\n            },\n        ];\n    }\n\n    addNames(options) {\n        const displayNames = Object.fromEntries(options);\n        this.nameService.addDisplayNames(this.props.resModel, displayNames);\n    }\n\n    getIds() {\n        return this.props.getIds();\n    }\n\n    async loadOptionsSource(name) {\n        if (this.lastProm) {\n            this.lastProm.abort(false);\n        }\n        this.lastProm = this.search(name, SEARCH_LIMIT + 1);\n        const nameGets = (await this.lastProm).map(([id, label]) => ([id, label ? label.split(\"\\n\")[0] : _t(\"Unnamed\")]));\n        this.addNames(nameGets);\n        const options = nameGets.map(([value, label]) => ({value, label}));\n        if (SEARCH_LIMIT < nameGets.length) {\n            options.push({\n                label: _t(\"Search More...\"),\n                action: this.onSearchMore.bind(this, name),\n                classList: \"o_m2o_dropdown_option\",\n            });\n        }\n        if (options.length === 0) {\n            options.push({ label: _t(\"(no result)\"), unselectable: true });\n        }\n        return options;\n    }\n\n    async onSearchMore(name) {\n        const { fieldString, multiSelect, resModel } = this.props;\n        let operator;\n        const ids = [];\n        if (name) {\n            const nameGets = await this.search(name, SEARCH_MORE_LIMIT);\n            this.addNames(nameGets);\n            operator = \"in\";\n            ids.push(...nameGets.map((nameGet) => nameGet[0]));\n        } else {\n            operator = \"not in\";\n            ids.push(...this.getIds());\n        }\n        const dynamicFilters = ids.length\n            ? [\n                  {\n                      description: _t(\"Quick search: %s\", name),\n                      domain: [[\"id\", operator, ids]],\n                  },\n              ]\n            : undefined;\n        // fine for now but we don't like this kind of dependence of core to views\n        const SelectCreateDialog = registry.category(\"dialogs\").get(\"select_create\");\n        this.addDialog(SelectCreateDialog, {\n            title: _t(\"Search: %s\", fieldString),\n            dynamicFilters,\n            domain: this.getDomain(),\n            resModel,\n            noCreate: true,\n            multiSelect,\n            context: this.props.context || {},\n            onSelected: (resId) => {\n                const resIds = Array.isArray(resId) ? resId : [resId];\n                this.props.update([...resIds]);\n            },\n        });\n    }\n\n    getDomain() {\n        const domainIds = Domain.not([[\"id\", \"in\", this.getIds()]]);\n        if (this.props.domain) {\n            return Domain.and([this.props.domain, domainIds]).toList();\n        }\n        return domainIds.toList();\n    }\n\n    onSelect({ value: resId, action }, params) {\n        if (action) {\n            return action(params);\n        }\n        this.props.update([resId]);\n    }\n\n    search(name, limit) {\n        const domain = this.getDomain();\n        return this.orm.call(this.props.resModel, \"name_search\", [], {\n            name,\n            args: domain,\n            limit,\n            context: this.props.context || {},\n        });\n    }\n\n    onChange({ inputValue }) {\n        if (!inputValue.length) {\n            this.props.update([]);\n        }\n    }\n}\n", "import { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { RecordAutocomplete } from \"./record_autocomplete\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class RecordSelector extends Component {\n    static props = {\n        resId: [Number, { value: false }],\n        resModel: String,\n        update: Function,\n        domain: { type: Array, optional: true },\n        context: { type: Object, optional: true },\n        fieldString: { type: String, optional: true },\n        placeholder: { type: String, optional: true },\n    };\n    static components = { RecordAutocomplete };\n    static template = \"web.RecordSelector\";\n\n    setup() {\n        this.nameService = useService(\"name\");\n        onWillStart(() => this.computeDerivedParams());\n        onWillUpdateProps((nextProps) => this.computeDerivedParams(nextProps));\n    }\n\n    async computeDerivedParams(props = this.props) {\n        const displayNames = await this.getDisplayNames(props);\n        this.displayName = this.getDisplayName(props, displayNames);\n    }\n\n    async getDisplayNames(props) {\n        const ids = this.getIds(props);\n        return this.nameService.loadDisplayNames(props.resModel, ids);\n    }\n\n    getDisplayName(props = this.props, displayNames) {\n        const { resId } = props;\n        if (resId === false) {\n            return \"\";\n        }\n        return typeof displayNames[resId] === \"string\"\n            ? displayNames[resId]\n            : _t(\"Inaccessible/missing record ID: %s\", resId);\n    }\n\n    getIds(props = this.props) {\n        if (props.resId) {\n            return [props.resId];\n        }\n        return [];\n    }\n\n    update(resIds) {\n        this.props.update(resIds[0] || false);\n        this.render(true);\n    }\n}\n", "import { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\n\nimport { useEffect, useRef } from \"@odoo/owl\";\n\n/**\n * This hook allows to navigate between tags in a record selector. It also\n * allows to delete tags with the backspace key.\n * It is meant to be used in component which contains both the components\n * `Autocomplete` and `TagList`.\n *\n * @param {string} refName Name of the t-ref which contains the `Autocomplete` and `TagList` components.\n * @param {Function} deleteTag Function to be called when a tag is deleted. It should take the index of the tag to delete as parameter.\n * @returns {Function} Function to be called when a tag is focused and a key is pressed. It should be passed to the `onKeydown` prop of the `Tag` component.\n */\nexport function useTagNavigation(refName, deleteTag) {\n    const ref = useRef(refName);\n\n    useEffect(\n        (autocomplete) => {\n            if (!autocomplete) {\n                return;\n            }\n            autocomplete.addEventListener(\"keydown\", onAutoCompleteKeydown);\n            return () => {\n                autocomplete.removeEventListener(\"keydown\", onAutoCompleteKeydown);\n            };\n        },\n        () => [ref.el?.querySelector(\".o-autocomplete\")]\n    );\n\n    /**\n     * Focus the tag at the given index. If no index is given, focus the rightmost tag.\n     * @param {number|undefined} index Index of the tag to focus. If undefined, focus the rightmost tag.\n     */\n    function focusTag(index) {\n        const tags = ref.el.getElementsByClassName(\"o_tag\");\n        if (tags.length) {\n            if (index === undefined) {\n                tags[tags.length - 1].focus();\n            } else {\n                tags[index].focus();\n            }\n        }\n    }\n\n    /**\n     * Function to be called when a key is pressed in the `Autocomplete` component.\n     *\n     * @param {Event} ev\n     */\n    function onAutoCompleteKeydown(ev) {\n        if (ev.isComposing) {\n            // This case happens with an IME for example: we let it handle all key events.\n            return;\n        }\n        const hotkey = getActiveHotkey(ev);\n        const input = ev.target.closest(\".o-autocomplete--input\");\n        const autoCompleteMenuOpened = !!ref.el.querySelector(\".o-autocomplete--dropdown-menu\");\n        switch (hotkey) {\n            case \"arrowleft\": {\n                if (input.selectionStart || autoCompleteMenuOpened) {\n                    return;\n                }\n                // focus rightmost tag if any.\n                focusTag();\n                break;\n            }\n            case \"arrowright\": {\n                if (input.selectionStart !== input.value.length || autoCompleteMenuOpened) {\n                    return;\n                }\n                // focus leftmost tag if any.\n                focusTag(0);\n                break;\n            }\n            case \"backspace\": {\n                if (input.value) {\n                    return;\n                }\n                const tags = ref.el.getElementsByClassName(\"o_tag\");\n                if (tags.length) {\n                    deleteTag(tags.length - 1);\n                }\n                break;\n            }\n            default:\n                return;\n        }\n        ev.preventDefault();\n        ev.stopPropagation();\n    }\n\n    /**\n     * Function to be called when a key is pressed in the `Tag` component.\n     * It should be passed to the `onKeydown` prop of the `Tag` component.\n     *\n     * @param {Event} ev\n     */\n    function onTagKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        const tags = [...ref.el.getElementsByClassName(\"o_tag\")];\n        const closestTag = ev.target.closest(\".o_tag\");\n        const tagIndex = tags.indexOf(closestTag);\n        const input = ref.el.querySelector(\".o-autocomplete--input\");\n        switch (hotkey) {\n            case \"arrowleft\": {\n                if (tagIndex === 0) {\n                    input.focus();\n                } else {\n                    focusTag(tagIndex - 1);\n                }\n                break;\n            }\n            case \"arrowright\": {\n                if (tagIndex === tags.length - 1) {\n                    input.focus();\n                } else {\n                    focusTag(tagIndex + 1);\n                }\n                break;\n            }\n            case \"backspace\": {\n                input.focus();\n                deleteTag(tagIndex);\n                break;\n            }\n            default:\n                return;\n        }\n        ev.preventDefault();\n        ev.stopPropagation();\n    }\n\n    return onTagKeydown;\n}\n", "import { EventBus, validate } from \"@odoo/owl\";\n\n// -----------------------------------------------------------------------------\n// Errors\n// -----------------------------------------------------------------------------\nexport class KeyNotFoundError extends Error {}\n\nexport class DuplicatedKeyError extends Error {}\n\n// -----------------------------------------------------------------------------\n// Validation\n// -----------------------------------------------------------------------------\n\nconst validateSchema = (value, schema) => {\n    if (!odoo.debug) {\n        return;\n    }\n    validate(value, schema);\n}\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * @template S\n * @template C\n * @typedef {import(\"registries\").RegistryData<S, C>} RegistryData\n */\n\n/**\n * @template T\n * @typedef {T extends RegistryData<any, any> ? T : RegistryData<T, {}>} ToRegistryData\n */\n\n/**\n * @template T\n * @typedef {ToRegistryData<T>[\"__itemShape\"]} GetRegistryItemShape\n */\n\n/**\n * @template T\n * @typedef {ToRegistryData<T>[\"__categories\"]} GetRegistryCategories\n */\n\n/**\n * Registry\n *\n * The Registry class is basically just a mapping from a string key to an object.\n * It is really not much more than an object. It is however useful for the\n * following reasons:\n *\n * 1. it let us react and execute code when someone add something to the registry\n *   (for example, the FunctionRegistry subclass this for this purpose)\n * 2. it throws an error when the get operation fails\n * 3. it provides a chained API to add items to the registry.\n *\n * @template T\n */\nexport class Registry extends EventBus {\n    /**\n     * @param {string} [name]\n     */\n    constructor(name) {\n        super();\n        /** @type {Record<string, [number, GetRegistryItemShape<T>]>}*/\n        this.content = {};\n        /** @type {{ [P in keyof GetRegistryCategories<T>]?: Registry<GetRegistryCategories<T>[P]> }} */\n        this.subRegistries = {};\n        /** @type {GetRegistryItemShape<T>[]}*/\n        this.elements = null;\n        /** @type {[string, GetRegistryItemShape<T>][]}*/\n        this.entries = null;\n        this.name = name;\n        this.validationSchema = null;\n\n        this.addEventListener(\"UPDATE\", () => {\n            this.elements = null;\n            this.entries = null;\n        });\n    }\n\n    /**\n     * Add an entry (key, value) to the registry if key is not already used. If\n     * the parameter force is set to true, an entry with same key (if any) is replaced.\n     *\n     * Note that this also returns the registry, so another add method call can\n     * be chained\n     *\n     * @param {string} key\n     * @param {GetRegistryItemShape<T>} value\n     * @param {{force?: boolean, sequence?: number}} [options]\n     * @returns {Registry<T>}\n     */\n    add(key, value, { force, sequence } = {}) {\n        if (this.validationSchema) {\n            validateSchema(value, this.validationSchema);\n        }\n        if (!force && key in this.content) {\n            throw new DuplicatedKeyError(\n                `Cannot add key \"${key}\" in the \"${this.name}\" registry: it already exists`\n            );\n        }\n        let previousSequence;\n        if (force) {\n            const elem = this.content[key];\n            previousSequence = elem && elem[0];\n        }\n        sequence = sequence === undefined ? previousSequence || 50 : sequence;\n        this.content[key] = [sequence, value];\n        const payload = { operation: \"add\", key, value };\n        this.trigger(\"UPDATE\", payload);\n        return this;\n    }\n\n    /**\n     * Get an item from the registry\n     *\n     * @param {string} key\n     * @returns {GetRegistryItemShape<T>}\n     */\n    get(key, defaultValue) {\n        if (arguments.length < 2 && !(key in this.content)) {\n            throw new KeyNotFoundError(`Cannot find key \"${key}\" in the \"${this.name}\" registry`);\n        }\n        const info = this.content[key];\n        return info ? info[1] : defaultValue;\n    }\n\n    /**\n     * Check the presence of a key in the registry\n     *\n     * @param {string} key\n     * @returns {boolean}\n     */\n    contains(key) {\n        return key in this.content;\n    }\n\n    /**\n     * Get a list of all elements in the registry. Note that it is ordered\n     * according to the sequence numbers.\n     *\n     * @returns {GetRegistryItemShape<T>[]}\n     */\n    getAll() {\n        if (!this.elements) {\n            const content = Object.values(this.content).sort((el1, el2) => el1[0] - el2[0]);\n            this.elements = content.map((elem) => elem[1]);\n        }\n        return this.elements.slice();\n    }\n\n    /**\n     * Return a list of all entries, ordered by sequence numbers.\n     *\n     * @returns {[string, GetRegistryItemShape<T>][]}\n     */\n    getEntries() {\n        if (!this.entries) {\n            const entries = Object.entries(this.content).sort((el1, el2) => el1[1][0] - el2[1][0]);\n            this.entries = entries.map(([str, elem]) => [str, elem[1]]);\n        }\n        return this.entries.slice();\n    }\n\n    /**\n     * Remove an item from the registry\n     *\n     * @param {string} key\n     */\n    remove(key) {\n        const value = this.content[key];\n        delete this.content[key];\n        const payload = { operation: \"delete\", key, value };\n        this.trigger(\"UPDATE\", payload);\n    }\n\n    /**\n     * Open a sub registry (and create it if necessary)\n     *\n     * @template {keyof GetRegistryCategories<T> & string} K\n     * @param {K} subcategory\n     * @returns {Registry<GetRegistryCategories<T>[K]>}\n     */\n    category(subcategory) {\n        if (!(subcategory in this.subRegistries)) {\n            this.subRegistries[subcategory] = new Registry(subcategory);\n        }\n        return this.subRegistries[subcategory];\n    }\n\n    addValidation(schema) {\n        if (this.validationSchema) {\n            throw new Error(\"Validation schema already set on this registry\");\n        }\n        this.validationSchema = schema;\n        for (const value of this.getAll()) {\n            validateSchema(value, schema);\n        }\n    }\n}\n\n/** @type {Registry<import(\"registries\").GlobalRegistry>} */\nexport const registry = new Registry();\n", "import { useState, onWillStart, onWillDestroy } from \"@odoo/owl\";\n\nexport function useRegistry(registry) {\n    const state = useState({ entries: registry.getEntries() });\n    const listener = ({ detail }) => {\n        const index = state.entries.findIndex(([k]) => k === detail.key);\n        if (detail.operation === \"add\" && index === -1) {\n            // push the new entry at the right place\n            const newEntries = registry.getEntries();\n            const newEntry = newEntries.find(([k]) => k === detail.key);\n            const newIndex = newEntries.indexOf(newEntry);\n            if (newIndex === newEntries.length - 1) {\n                state.entries.push(newEntry);\n            } else {\n                state.entries.splice(newIndex, 0, newEntry);\n            }\n        } else if (detail.operation === \"delete\" && index >= 0) {\n            state.entries.splice(index, 1);\n        }\n    };\n\n    onWillStart(() => registry.addEventListener(\"UPDATE\", listener));\n    onWillDestroy(() => registry.removeEventListener(\"UPDATE\", listener));\n    return state;\n}\n", "import {\n    Component,\n    onMounted,\n    onWillUpdateProps,\n    onWillUnmount,\n    useEffect,\n    useExternalListener,\n    useRef,\n    useComponent,\n} from \"@odoo/owl\";\n\nfunction useResizable({\n    containerRef,\n    handleRef,\n    initialWidth = 400,\n    getMinWidth = () => 400,\n    onResize = () => {},\n    getResizeSide = () => \"end\",\n}) {\n    containerRef = typeof containerRef == \"string\" ? useRef(containerRef) : containerRef;\n    handleRef = typeof handleRef == \"string\" ? useRef(handleRef) : handleRef;\n    const props = useComponent().props;\n\n    let minWidth = getMinWidth(props);\n    let resizeSide = getResizeSide(props);\n    let isChangingSize = false;\n\n    useExternalListener(document, \"mouseup\", () => onMouseUp());\n    useExternalListener(document, \"mousemove\", (ev) => onMouseMove(ev));\n\n    useExternalListener(window, \"resize\", () => {\n        const limit = getLimitWidth();\n        if (getContainerRect().width >= limit) {\n            resize(computeFinalWidth(limit));\n        }\n    });\n\n    let docDirection;\n    useEffect(\n        (container) => {\n            if (container) {\n                docDirection = getComputedStyle(container).direction;\n            }\n        },\n        () => [containerRef.el]\n    );\n\n    onMounted(() => {\n        if (handleRef.el) {\n            resize(initialWidth);\n            handleRef.el.addEventListener(\"mousedown\", onMouseDown);\n        }\n    });\n\n    onWillUpdateProps((nextProps) => {\n        minWidth = getMinWidth(nextProps);\n        resizeSide = getResizeSide(nextProps);\n    });\n\n    onWillUnmount(() => {\n        if (handleRef.el) {\n            handleRef.el.removeEventListener(\"mousedown\", onMouseDown);\n        }\n    });\n\n    function onMouseDown() {\n        isChangingSize = true;\n        document.body.classList.add(\"pe-none\", \"user-select-none\");\n    }\n\n    function onMouseUp() {\n        isChangingSize = false;\n        document.body.classList.remove(\"pe-none\", \"user-select-none\");\n    }\n\n    function onMouseMove(ev) {\n        if (!isChangingSize || !containerRef.el) {\n            return;\n        }\n        const direction =\n            (docDirection === \"ltr\" && resizeSide === \"end\") ||\n            (docDirection === \"rtl\" && resizeSide === \"start\")\n                ? 1\n                : -1;\n        const fixedSide = direction === 1 ? \"left\" : \"right\";\n        const containerRect = getContainerRect();\n        const newWidth = (ev.clientX - containerRect[fixedSide]) * direction;\n        resize(computeFinalWidth(newWidth));\n    }\n\n    function computeFinalWidth(targetContainerWidth) {\n        const handlerSpacing = handleRef.el ? handleRef.el.offsetWidth / 2 : 10;\n        const w = Math.max(minWidth, targetContainerWidth + handlerSpacing);\n        const limit = getLimitWidth();\n        return Math.min(w, limit - handlerSpacing);\n    }\n\n    function getContainerRect() {\n        const container = containerRef.el;\n        const offsetParent = container.offsetParent;\n        let containerRect = {};\n        if (!offsetParent) {\n            containerRect = container.getBoundingClientRect();\n        } else {\n            containerRect.left = container.offsetLeft;\n            containerRect.right = container.offsetLeft + container.offsetWidth;\n            containerRect.width = container.offsetWidth;\n        }\n        return containerRect;\n    }\n\n    function getLimitWidth() {\n        const offsetParent = containerRef.el.offsetParent;\n        return offsetParent ? offsetParent.offsetWidth : window.innerWidth;\n    }\n\n    function resize(width) {\n        containerRef.el.style.setProperty(\"width\", `${width}px`);\n        onResize(width);\n    }\n}\n\nexport class ResizablePanel extends Component {\n    static template = \"web_studio.ResizablePanel\";\n\n    static components = {};\n    static props = {\n        onResize: { type: Function, optional: true },\n        initialWidth: { type: Number, optional: true },\n        minWidth: { type: Number, optional: true },\n        class: { type: String, optional: true },\n        slots: { type: Object },\n        handleSide: {\n            validate: (val) => [\"start\", \"end\"].includes(val),\n            optional: true,\n        },\n    };\n    static defaultProps = {\n        onResize: () => {},\n        width: 400,\n        minWidth: 400,\n        class: \"\",\n        handleSide: \"end\",\n    };\n\n    setup() {\n        useResizable({\n            containerRef: \"containerRef\",\n            handleRef: \"handleRef\",\n            onResize: this.props.onResize,\n            initialWidth: this.props.initialWidth,\n            getMinWidth: (props) => props.minWidth,\n            getResizeSide: (props) => props.handleSide,\n        });\n    }\n\n    get class() {\n        const classes = this.props.class.split(\" \");\n        if (!classes.some((cls) => cls.startsWith(\"position-\"))) {\n            classes.push(\"position-relative\");\n        }\n        return classes.join(\" \");\n    }\n}\n", "import { Component, onWillUpdateProps, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { mergeClasses } from \"@web/core/utils/classname\";\nimport { useAutofocus, useChildRef } from \"@web/core/utils/hooks\";\nimport { scrollTo } from \"@web/core/utils/scrolling\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { useDebounced } from \"@web/core/utils/timing\";\n\nexport class SelectMenu extends Component {\n    static template = \"web.SelectMenu\";\n    static choiceItemTemplate = \"web.SelectMenu.ChoiceItem\";\n\n    static components = { Dropdown, DropdownItem, TagsList };\n\n    static defaultProps = {\n        value: undefined,\n        class: \"\",\n        togglerClass: \"\",\n        multiSelect: false,\n        onSelect: () => {},\n        required: false,\n        searchable: true,\n        autoSort: true,\n        searchPlaceholder: _t(\"Search...\"),\n        choices: [],\n        groups: [],\n        disabled: false,\n    };\n\n    static props = {\n        choices: {\n            optional: true,\n            type: Array,\n            element: {\n                type: Object,\n                shape: {\n                    value: true,\n                    label: { type: String },\n                    \"*\": true,\n                },\n            },\n        },\n        groups: {\n            type: Array,\n            optional: true,\n            element: {\n                type: Object,\n                shape: {\n                    label: { type: String, optional: true },\n                    choices: {\n                        type: Array,\n                        element: {\n                            type: Object,\n                            shape: {\n                                value: true,\n                                label: { type: String },\n                                \"*\": true,\n                            },\n                        },\n                    },\n                },\n            },\n        },\n        class: { type: String, optional: true },\n        menuClass: { type: String, optional: true },\n        togglerClass: { type: String, optional: true },\n        required: { type: Boolean, optional: true },\n        searchable: { type: Boolean, optional: true },\n        autoSort: { type: Boolean, optional: true },\n        placeholder: { type: String, optional: true },\n        searchPlaceholder: { type: String, optional: true },\n        value: { optional: true },\n        multiSelect: { type: Boolean, optional: true },\n        onInput: { type: Function, optional: true },\n        onSelect: { type: Function, optional: true },\n        slots: { type: Object, optional: true },\n        disabled: { type: Boolean, optional: true },\n    };\n\n    static SCROLL_SETTINGS = {\n        defaultCount: 500,\n        increaseAmount: 300,\n        distanceBeforeReload: 500,\n    };\n\n    setup() {\n        this.state = useState({\n            choices: [],\n            displayedOptions: [],\n            searchValue: \"\",\n        });\n        this.inputRef = useRef(\"inputRef\");\n        this.menuRef = useChildRef();\n        this.debouncedOnInput = useDebounced(\n            () => this.onInput(this.inputRef.el ? this.inputRef.el.value.trim() : \"\"),\n            250\n        );\n        this.isOpen = false;\n\n        this.selectedChoice = this.getSelectedChoice(this.props);\n        onWillUpdateProps((nextProps) => {\n            if (this.state.choices !== nextProps.choices) {\n                this.state.choices = nextProps.choices;\n            }\n            if (this.props.value !== nextProps.value) {\n                this.selectedChoice = this.getSelectedChoice(nextProps);\n            }\n        });\n        useEffect(\n            () => {\n                if (this.isOpen) {\n                    const groups = [{ choices: this.props.choices }, ...this.props.groups];\n                    this.filterOptions(this.state.searchValue, groups);\n                }\n            },\n            () => [this.props.choices, this.props.groups]\n        );\n        useAutofocus({ refName: \"inputRef\" });\n    }\n\n    get displayValue() {\n        return this.selectedChoice ? this.selectedChoice.label : \"\";\n    }\n\n    get canDeselect() {\n        return !this.props.required && this.selectedChoice !== undefined;\n    }\n\n    get multiSelectChoices() {\n        return this.selectedChoice.map((c) => {\n            return {\n                id: c.value,\n                text: c.label,\n                onDelete: () => {\n                    const values = [...this.props.value];\n                    values.splice(values.indexOf(c.value), 1);\n                    this.props.onSelect(values);\n                },\n            };\n        });\n    }\n\n    get menuClass() {\n        return mergeClasses(\n            {\n                \"o_select_menu_menu border bg-light\": true,\n                \"py-0\": this.props.searchable,\n                o_select_menu_multi_select: this.props.multiSelect,\n            },\n            this.props.menuClass\n        );\n    }\n\n    async onBeforeOpen() {\n        if (this.state.searchValue.length) {\n            this.state.searchValue = \"\";\n            if (this.props.onInput) {\n                // This props can be used by the parent to fetch items dynamically depending\n                // the search value. It must be called with the empty search value.\n                await this.executeOnInput(\"\");\n            }\n        }\n        this.filterOptions();\n    }\n\n    onStateChanged(open) {\n        this.isOpen = open;\n        if (open) {\n            this.menuRef.el?.addEventListener(\"scroll\", (ev) => this.onScroll(ev));\n            const selectedElement = this.menuRef.el?.querySelectorAll(\".o_select_active\")[0];\n            if (selectedElement) {\n                scrollTo(selectedElement);\n            }\n        }\n    }\n\n    isOptionSelected(choice) {\n        if (this.props.multiSelect) {\n            return this.props.value.includes(choice.value);\n        }\n        return this.props.value === choice.value;\n    }\n\n    getItemClass(choice) {\n        if (this.isOptionSelected(choice)) {\n            return \"o_select_menu_item p-2 o_select_active bg-primary fw-bolder fst-italic\";\n        } else {\n            return \"o_select_menu_item p-2\";\n        }\n    }\n\n    async executeOnInput(searchString) {\n        await this.props.onInput(searchString);\n    }\n\n    onInput(searchString) {\n        this.filterOptions(searchString);\n        this.state.searchValue = searchString;\n\n        // Get reference to dropdown container and scroll to the top.\n        const inputEl = this.inputRef.el;\n        if (inputEl && inputEl.parentNode) {\n            inputEl.parentNode.scrollTo(0, 0);\n        }\n        if (this.props.onInput) {\n            this.executeOnInput(searchString);\n        }\n    }\n\n    getSelectedChoice(props) {\n        const choices = [...props.choices, ...props.groups.flatMap((g) => g.choices)];\n        if (!this.props.multiSelect) {\n            return choices.find((c) => c.value === props.value);\n        }\n\n        const valueSet = new Set(props.value);\n        // Combine previously selected choices + newly selected choice from\n        // the searched choices and then filter the choices based on\n        // props.value i.e. valueSet.\n        return [...(this.selectedChoice || []), ...choices].filter((c, index, self) =>\n            valueSet.has(c.value)\n            && self.findIndex((t) => t.value === c.value) === index\n        );\n    }\n\n    onItemSelected(value) {\n        if (this.props.multiSelect) {\n            const values = [...this.props.value];\n            const valueIndex = values.indexOf(value);\n\n            if (valueIndex !== -1) {\n                values.splice(valueIndex, 1);\n                this.props.onSelect(values);\n            } else {\n                this.props.onSelect([...this.props.value, value]);\n            }\n        } else if (!this.selectedChoice || this.selectedChoice.value !== value) {\n            this.props.onSelect(value);\n        }\n        if (this.inputRef.el) {\n            this.inputRef.el.value = \"\";\n            this.state.searchValue = \"\";\n        }\n    }\n\n    // ==========================================================================================\n    // #                                         Search                                         #\n    // ==========================================================================================\n\n    /**\n     * Filters the choices based on the searchString and\n     * slice the result to display a reasonable amount to\n     * try to prevent any delay when opening the select.\n     *\n     * @param {String} searchString\n     */\n    filterOptions(searchString = \"\", groups) {\n        const groupsList = groups || [{ choices: this.props.choices }, ...this.props.groups];\n\n        this.state.choices = [];\n\n        for (const group of groupsList) {\n            let filteredOptions = [];\n\n            if (searchString) {\n                filteredOptions = fuzzyLookup(\n                    searchString,\n                    group.choices,\n                    (choice) => choice.label\n                );\n            } else {\n                filteredOptions = group.choices;\n                if (this.props.autoSort) {\n                    filteredOptions.sort((optionA, optionB) =>\n                        optionA.label.localeCompare(optionB.label)\n                    );\n                }\n            }\n\n            if (filteredOptions.length === 0) {\n                continue;\n            }\n\n            if (group.label) {\n                this.state.choices.push({ ...group, isGroup: true });\n            }\n            this.state.choices.push(...filteredOptions);\n        }\n\n        this.sliceDisplayedOptions();\n    }\n\n    // ==========================================================================================\n    // #                                         Scroll                                         #\n    // ==========================================================================================\n\n    /**\n     * If the user scrolls to the end of the dropdown,\n     * more choices are loaded.\n     *\n     * @param {*} event\n     */\n    onScroll(event) {\n        const el = event.target;\n        const hasReachMax = this.state.displayedOptions.length >= this.state.choices.length;\n        const remainingDistance = el.scrollHeight - el.scrollTop;\n        const distanceToReload =\n            el.clientHeight + this.constructor.SCROLL_SETTINGS.distanceBeforeReload;\n\n        if (!hasReachMax && remainingDistance < distanceToReload) {\n            const displayCount =\n                this.state.displayedOptions.length +\n                this.constructor.SCROLL_SETTINGS.increaseAmount;\n\n            this.state.displayedOptions = this.state.choices.slice(0, displayCount);\n        }\n    }\n\n    /**\n     * Finds the selected choice and set [displayOptions] to at\n     * least show the selected choice and [defaultCount] more\n     * or show at least the [defaultDisplayCount].\n     */\n    sliceDisplayedOptions() {\n        const selectedIndex = this.getSelectedOptionIndex();\n        const defaultCount = this.constructor.SCROLL_SETTINGS.defaultCount;\n\n        if (selectedIndex === -1) {\n            this.state.displayedOptions = this.state.choices.slice(0, defaultCount);\n        } else {\n            const endIndex = Math.max(\n                selectedIndex + this.constructor.SCROLL_SETTINGS.increaseAmount,\n                defaultCount\n            );\n            this.state.displayedOptions = this.state.choices.slice(0, endIndex);\n        }\n    }\n\n    getSelectedOptionIndex() {\n        let selectedIndex = -1;\n        for (let i = 0; i < this.state.choices.length; i++) {\n            if (this.isOptionSelected(this.state.choices[i])) {\n                selectedIndex = i;\n            }\n        }\n        return selectedIndex;\n    }\n}\n", "/* global SignaturePad */\n\nimport { loadJS } from \"@web/core/assets\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\nimport { renderToString } from \"@web/core/utils/render\";\nimport { getDataURLFromFile } from \"@web/core/utils/urls\";\n\nimport { Component, useState, onWillStart, useRef, useEffect } from \"@odoo/owl\";\n\nlet htmlId = 0;\nexport class NameAndSignature extends Component {\n    static template = \"web.NameAndSignature\";\n    static components = { Dropdown, DropdownItem };\n    static props = {\n        signature: { type: Object },\n        defaultFont: { type: String, optional: true },\n        displaySignatureRatio: { type: Number, optional: true },\n        fontColor: { type: String, optional: true },\n        signatureType: { type: String, optional: true },\n        noInputName: { type: Boolean, optional: true },\n        mode: { type: String, optional: true },\n    };\n    static defaultProps = {\n        defaultFont: \"\",\n        displaySignatureRatio: 3.0,\n        fontColor: \"DarkBlue\",\n        signatureType: \"signature\",\n        noInputName: false,\n    };\n\n    setup() {\n        this.htmlId = htmlId++;\n        this.defaultName = this.props.signature.name || \"\";\n        this.currentFont = 0;\n        this.drawTimeout = null;\n\n        this.state = useState({\n            signMode:\n                this.props.mode || (this.props.noInputName && !this.defaultName ? \"draw\" : \"auto\"),\n            showSignatureArea: !!(this.props.noInputName || this.defaultName),\n            showFontList: false,\n        });\n\n        this.signNameInputRef = useRef(\"signNameInput\");\n        this.signInputLoad = useRef(\"signInputLoad\");\n        useAutofocus({ refName: \"signNameInput\" });\n        useEffect(\n            (el) => {\n                if (el) {\n                    el.click();\n                }\n            },\n            () => [this.signInputLoad.el]\n        );\n\n        onWillStart(async () => {\n            this.fonts = await rpc(`/web/sign/get_fonts/${this.props.defaultFont}`);\n        });\n\n        onWillStart(async () => {\n            await loadJS(\"/web/static/lib/signature_pad/signature_pad.umd.js\");\n        });\n\n        this.signatureRef = useRef(\"signature\");\n        useEffect(\n            (el) => {\n                if (el) {\n                    this.signaturePad = new SignaturePad(el, {\n                        penColor: this.props.fontColor,\n                        backgroundColor: \"rgba(255,255,255,0)\",\n                        minWidth: 2,\n                        maxWidth: 2,\n                    });\n                    this.signaturePad.addEventListener(\"endStroke\", () => {\n                        this.props.signature.isSignatureEmpty = this.isSignatureEmpty;\n                    });\n                    this.resetSignature();\n                    this.props.signature.getSignatureImage = () => this.signaturePad.toDataURL();\n                    this.props.signature.resetSignature = () => this.resetSignature();\n                    if (this.state.signMode === \"auto\") {\n                        this.drawCurrentName();\n                    }\n                    if (this.props.signature.signatureImage) {\n                        this.clear();\n                        this.signaturePad.fromDataURL(this.props.signature.signatureImage);\n                    }\n                }\n            },\n            () => [this.signatureRef.el]\n        );\n    }\n\n    /**\n     * Draws the current name with the current font in the signature field.\n     */\n    async drawCurrentName() {\n        const font = this.fonts[this.currentFont];\n        const text = this.getCleanedName();\n        const canvas = this.signatureRef.el;\n        const img = this.getSVGText(font, text, canvas.width, canvas.height);\n        await this.printImage(img);\n    }\n\n    focusName() {\n        // Don't focus on mobile\n        if (!isMobileOS() && this.signNameInputRef.el) {\n            this.signNameInputRef.el.focus();\n        }\n    }\n\n    /**\n     * Clear the signature field.\n     */\n    clear() {\n        this.signaturePad.clear();\n        this.props.signature.isSignatureEmpty = this.isSignatureEmpty;\n    }\n\n    /**\n     * Returns the given name after cleaning it by removing characters that\n     * are not supposed to be used in a signature. If @see signatureType is set\n     * to 'initial', returns the first letter of each word, separated by dots.\n     *\n     * @returns {string} cleaned name\n     */\n    getCleanedName() {\n        const text = this.props.signature.name;\n        if (this.props.signatureType === \"initial\" && text) {\n            return (\n                text\n                    .split(\" \")\n                    .map(function (w) {\n                        return w[0];\n                    })\n                    .join(\".\") + \".\"\n            );\n        }\n        return text;\n    }\n\n    /**\n     * Gets an SVG matching the given parameters, output compatible with the\n     * src attribute of <img/>.\n     *\n     * @private\n     * @param {string} font: base64 encoded font to use\n     * @param {string} text: the name to draw\n     * @param {number} width: the width of the resulting image in px\n     * @param {number} height: the height of the resulting image in px\n     * @returns {string} image = mimetype + image data\n     */\n    getSVGText(font, text, width, height) {\n        const svg = renderToString(\"web.sign_svg_text\", {\n            width: width,\n            height: height,\n            font: font,\n            text: text,\n            type: this.props.signatureType,\n            color: this.props.fontColor,\n        });\n\n        return \"data:image/svg+xml,\" + encodeURI(svg);\n    }\n\n    getSVGTextFont(font) {\n        const height = 100;\n        const width = parseInt(height * this.props.displaySignatureRatio);\n        return this.getSVGText(font, this.getCleanedName(), width, height);\n    }\n\n    uploadFile() {\n        this.signInputLoad.el?.click();\n    }\n\n    /**\n     * Handles change on load file input: displays the loaded image if the\n     * format is correct, or displays an error otherwise.\n     *\n     * @see mode 'load'\n     * @private\n     * @param {Event} ev\n     * @return bool|undefined\n     */\n    async onChangeSignLoadInput(ev) {\n        var file = ev.target.files[0];\n        if (file === undefined) {\n            return false;\n        }\n        if (file.type.substr(0, 5) !== \"image\") {\n            this.clear();\n            this.state.loadIsInvalid = true;\n            return false;\n        }\n        this.state.loadIsInvalid = false;\n\n        const result = await getDataURLFromFile(file);\n        await this.printImage(result);\n    }\n\n    onClickSignAutoSelectStyle() {\n        this.state.showFontList = true;\n    }\n\n    onClickSignDrawClear() {\n        this.clear();\n    }\n\n    onClickSignLoad() {\n        this.setMode(\"load\");\n    }\n\n    onClickSignAuto() {\n        this.setMode(\"auto\");\n    }\n\n    onInputSignName(ev) {\n        this.props.signature.name = ev.target.value;\n        if (!this.state.showSignatureArea && this.getCleanedName()) {\n            this.state.showSignatureArea = true;\n            return;\n        }\n        if (this.state.signMode === \"auto\") {\n            this.drawCurrentName();\n        }\n    }\n\n    onSelectFont(index) {\n        this.currentFont = index;\n        this.drawCurrentName();\n    }\n\n    /**\n     * Displays the given image in the signature field.\n     * If needed, resizes the image to fit the existing area.\n     *\n     * @param {string} imgSrc - data of the image to display\n     */\n    async printImage(imgSrc) {\n        this.clear();\n        const c = this.signaturePad.canvas;\n        const img = new Image();\n        img.onload = () => {\n            const ctx = c.getContext(\"2d\");\n            var ratio = ((img.width / img.height) > (c.width / c.height)) ? c.width / img.width : c.height / img.height;\n            ctx.drawImage( \n                img,\n                (c.width / 2) - (img.width * ratio / 2),\n                (c.height / 2) - (img.height * ratio / 2)\n                , img.width * ratio\n                , img.height * ratio\n            );\n            this.props.signature.isSignatureEmpty = this.isSignatureEmpty;\n        };\n        img.src = imgSrc;\n        this.signaturePad._isEmpty = false;\n    }\n\n    /**\n     * (Re)initializes the signature area:\n     *  - set the correct width and height of the drawing based on the width\n     *      of the container and the ratio option\n     *  - empty any previous content\n     *  - correctly reset the empty state\n     *  - call @see setMode with reset\n     */\n    resetSignature() {\n        this.resizeSignature();\n        this.clear();\n        this.setMode(this.state.signMode, true);\n        this.focusName();\n    }\n\n    resizeSignature() {\n        // recompute size based on the current width\n        const width = this.signatureRef.el.clientWidth;\n        const height = parseInt(width / this.props.displaySignatureRatio);\n\n        Object.assign(this.signatureRef.el, { width, height });\n    }\n\n    /**\n     * Changes the signature mode. Toggles the display of the relevant\n     * controls and resets the drawing.\n     *\n     * @param {string} mode - the mode to use. Can be one of the following:\n     *  - 'draw': the user draws the signature manually with the mouse\n     *  - 'auto': the signature is drawn automatically using a selected font\n     *  - 'load': the signature is loaded from an image file\n     * @param {boolean} [reset=false] - Set to true to reset the elements\n     *  even if the @see mode has not changed. By default nothing happens\n     *  if the @see mode is already selected.\n     */\n    setMode(mode, reset) {\n        if (reset !== true && mode === this.signMode) {\n            // prevent flickering and unnecessary compute\n            return;\n        }\n\n        this.state.signMode = mode;\n        this.signaturePad[this.state.signMode === \"draw\" ? \"on\" : \"off\"]();\n        this.clear();\n\n        if (this.state.signMode === \"auto\") {\n            // draw based on name\n            this.drawCurrentName();\n        }\n    }\n\n    /**\n     * Returns whether the drawing area is currently empty.\n     *\n     * @returns {boolean} Whether the drawing area is currently empty.\n     */\n    get isSignatureEmpty() {\n        return this.signaturePad.isEmpty();\n    }\n\n    get loadIsInvalid() {\n        return this.state.signMode === \"load\" && this.state.loadIsInvalid;\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { NameAndSignature } from \"./name_and_signature\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class SignatureDialog extends Component {\n    static template = \"web.SignatureDialog\";\n    static components = { Dialog, NameAndSignature };\n    static props = {\n        defaultName: { type: String, optional: true },\n        nameAndSignatureProps: Object,\n        uploadSignature: Function,\n        close: Function,\n    };\n    static defaultProps = {\n        defaultName: \"\",\n    };\n\n    setup() {\n        this.signature = useState({\n            name: this.props.defaultName,\n            isSignatureEmpty: true,\n        });\n    }\n\n    /**\n     * Upload the signature image when confirm.\n     *\n     * @private\n     */\n    onClickConfirm() {\n        this.props.uploadSignature({\n            name: this.signature.name,\n            signatureImage: this.signature.getSignatureImage(),\n        });\n        this.props.close();\n    }\n\n    get nameAndSignatureProps() {\n        return {\n            ...this.props.nameAndSignatureProps,\n            signature: this.signature,\n        };\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class TagsList extends Component {\n    static template = \"web.TagsList\";\n    static defaultProps = {\n        displayText: true,\n    };\n    static props = {\n        displayText: { type: Boolean, optional: true },\n        visibleItemsLimit: { type: Number, optional: true },\n        tags: { type: Object },\n    };\n    get visibleTagsCount() {\n        return this.props.visibleItemsLimit - 1;\n    }\n    get visibleTags() {\n        if (this.props.visibleItemsLimit && this.props.tags.length > this.props.visibleItemsLimit) {\n            return this.props.tags.slice(0, this.visibleTagsCount);\n        }\n        return this.props.tags;\n    }\n    get otherTags() {\n        if (\n            !this.props.visibleItemsLimit ||\n            this.props.tags.length <= this.props.visibleItemsLimit\n        ) {\n            return [];\n        }\n        return this.props.tags.slice(this.visibleTagsCount);\n    }\n    get tooltipInfo() {\n        return JSON.stringify({\n            tags: this.otherTags.map((tag) => ({\n                text: tag.text,\n                id: tag.id,\n            })),\n        });\n    }\n}\n", "const RSTRIP_REGEXP = /(?=\\n[ \\t]*$)/;\n/**\n * The child nodes of operation represent new content to create before target or\n * or other elements to move before target from the target tree (tree from which target is part of).\n * Some processing of text nodes has to be done in order to normalize the situation.\n * Note: we assume that target has a parent element.\n * @param {Element} target\n * @param {Element} operation\n */\nfunction addBefore(target, operation) {\n    const nodes = getNodes(target, operation);\n    if (nodes.length === 0) {\n        return;\n    }\n    const { previousSibling } = target;\n    target.before(...nodes);\n    if (previousSibling?.nodeType === Node.TEXT_NODE) {\n        const [text1, text2] = previousSibling.data.split(RSTRIP_REGEXP);\n        previousSibling.data = text1.trimEnd();\n        if (nodes[0].nodeType === Node.TEXT_NODE) {\n            mergeTextNodes(previousSibling, nodes[0]);\n        }\n        if (text2 && nodes.some((n) => n.nodeType !== Node.TEXT_NODE)) {\n            const textNode = document.createTextNode(text2);\n            target.before(textNode);\n            if (textNode.previousSibling.nodeType === Node.TEXT_NODE) {\n                mergeTextNodes(textNode.previousSibling, textNode);\n            }\n        }\n    }\n}\n\n/**\n * element is part of a tree. Here we return the root element of that tree.\n * Note: this root element is not necessarily the documentElement of the ownerDocument\n * of element (hence the following code).\n * @param {Element} element\n * @returns {Element}\n */\nfunction getRoot(element) {\n    while (element.parentElement) {\n        element = element.parentElement;\n    }\n    return element;\n}\n\nconst HASCLASS_REGEXP = /hasclass\\(([^)]*)\\)/g;\n/**\n * @param {Element} operation\n * @returns {string}\n */\nfunction getXpath(operation) {\n    const xpath = operation.getAttribute(\"expr\");\n    // hasclass does not exist in XPath 1.0 but is a custom function defined server side (see _hasclass) usable in lxml.\n    // Here we have to replace it by a complex condition (which is not nice).\n    // Note: we assume that classes do not contain the 2 chars , and )\n    return xpath.replaceAll(HASCLASS_REGEXP, (_, capturedGroup) => {\n        return capturedGroup\n            .split(\",\")\n            .map((c) => `contains(concat(' ', @class, ' '), ' ${c.trim().slice(1, -1)} ')`)\n            .join(\" and \");\n    });\n}\n\n/**\n * @param {Element} element\n * @param {Element} operation\n * @returns {Node|null}\n */\nfunction getNode(element, operation) {\n    const root = getRoot(element);\n    const doc = new Document();\n    doc.appendChild(root); // => root is the documentElement of its ownerDocument (we do that in case root is a clone)\n    if (operation.tagName === \"xpath\") {\n        const xpath = getXpath(operation);\n        const result = doc.evaluate(xpath, root, null, XPathResult.FIRST_ORDERED_NODE_TYPE);\n        return result.singleNodeValue;\n    }\n    for (const elem of root.querySelectorAll(operation.tagName)) {\n        if (\n            [...operation.attributes].every(\n                ({ name, value }) => name === \"position\" || elem.getAttribute(name) === value\n            )\n        ) {\n            return elem;\n        }\n    }\n    return null;\n}\n\n/**\n * @param {Element} element\n * @param {Element} operation\n * @returns {Element}\n */\nfunction getElement(element, operation) {\n    const node = getNode(element, operation);\n    if (!node) {\n        throw new Error(`Element '${operation.outerHTML}' cannot be located in element tree`);\n    }\n    if (!(node instanceof Element)) {\n        throw new Error(`Found node ${node} instead of an element`);\n    }\n    return node;\n}\n\n/**\n * @param {Element} element\n * @param {Element} operation\n * @returns {Node[]}\n */\nfunction getNodes(element, operation) {\n    const nodes = [];\n    for (const childNode of operation.childNodes) {\n        if (childNode.tagName === \"xpath\" && childNode.getAttribute?.(\"position\") === \"move\") {\n            const node = getElement(element, childNode);\n            removeNode(node);\n            nodes.push(node);\n        } else {\n            nodes.push(childNode);\n        }\n    }\n    return nodes;\n}\n\n/**\n * @param {Text} first\n * @param {Text} second\n * @param {boolean} [trimEnd=true]\n */\nfunction mergeTextNodes(first, second, trimEnd = true) {\n    first.data = (trimEnd ? first.data.trimEnd() : first.data) + second.data;\n    second.remove();\n}\n\nfunction splitAndTrim(str, separator) {\n    return str.split(separator).map((s) => s.trim());\n}\n\n/**\n * @param {Element} target\n * @param {Element} operation\n */\nfunction modifyAttributes(target, operation) {\n    for (const child of operation.children) {\n        if (child.tagName !== \"attribute\") {\n            continue;\n        }\n        const attributeName = child.getAttribute(\"name\");\n        const firstNode = child.childNodes[0];\n        let value = firstNode?.nodeType === Node.TEXT_NODE ? firstNode.data : \"\";\n\n        const add = child.getAttribute(\"add\") || \"\";\n        const remove = child.getAttribute(\"remove\") || \"\";\n        if (add || remove) {\n            if (firstNode?.nodeType === Node.TEXT_NODE) {\n                throw new Error(`Useless element content ${firstNode.outerHTML}`);\n            }\n            const separator = child.getAttribute(\"separator\") || \",\";\n            const toRemove = new Set(splitAndTrim(remove, separator));\n            const values = splitAndTrim(target.getAttribute(attributeName) || \"\", separator).filter(\n                (s) => !toRemove.has(s)\n            );\n            values.push(...splitAndTrim(add, separator).filter((s) => s));\n            value = values.join(separator);\n        }\n\n        if (value) {\n            target.setAttribute(attributeName, value);\n        } else {\n            target.removeAttribute(attributeName);\n        }\n    }\n}\n\n/**\n * Remove node and normalize surrounind text nodes (if any)\n * Note: we assume that node has a parent element\n * @param {Node} node\n */\nfunction removeNode(node) {\n    const { nextSibling, previousSibling } = node;\n    node.remove();\n    if (nextSibling?.nodeType === Node.TEXT_NODE && previousSibling?.nodeType === Node.TEXT_NODE) {\n        mergeTextNodes(\n            previousSibling,\n            nextSibling,\n            previousSibling.parentElement.firstChild === previousSibling\n        );\n    }\n}\n\n/**\n * @param {Element} root\n * @param {Element} target\n * @param {Element} operation\n */\nfunction replace(root, target, operation) {\n    const mode = operation.getAttribute(\"mode\") || \"outer\";\n    switch (mode) {\n        case \"outer\": {\n            const result = operation.ownerDocument.evaluate(\n                \".//*[text()='$0']\",\n                operation,\n                null,\n                XPathResult.ORDERED_NODE_SNAPSHOT_TYPE\n            );\n            for (let i = 0; i < result.snapshotLength; i++) {\n                const loc = result.snapshotItem(i);\n                loc.firstChild.replaceWith(target.cloneNode(true));\n            }\n            if (target.parentElement) {\n                const nodes = getNodes(target, operation);\n                target.replaceWith(...nodes);\n            } else {\n                let operationContent = null;\n                let comment = null;\n                for (const child of operation.childNodes) {\n                    if (child.nodeType === Node.ELEMENT_NODE) {\n                        operationContent = child;\n                        break;\n                    }\n                    if (child.nodeType === Node.COMMENT_NODE) {\n                        comment = child;\n                    }\n                }\n                root = operationContent.cloneNode(true);\n                if (target.hasAttribute(\"t-name\")) {\n                    root.setAttribute(\"t-name\", target.getAttribute(\"t-name\"));\n                }\n                if (comment) {\n                    root.prepend(comment);\n                }\n            }\n            break;\n        }\n        case \"inner\":\n            while (target.firstChild) {\n                target.removeChild(target.lastChild);\n            }\n            target.append(...operation.childNodes);\n            break;\n        default:\n            throw new Error(`Invalid mode attribute: '${mode}'`);\n    }\n    return root;\n}\n\n/**\n * @param {Element} root\n * @param {Element} operations is a single element whose children represent operations to perform on root\n * @param {string} [url=\"\"]\n * @returns {Element} root modified (in place) by the operations\n */\nexport function applyInheritance(root, operations, url = \"\") {\n    for (const operation of operations.children) {\n        const target = getElement(root, operation);\n        const position = operation.getAttribute(\"position\") || \"inside\";\n\n        if (odoo.debug && url) {\n            const attributes = [...operation.attributes].map(\n                ({ name, value }) =>\n                    `${name}=${JSON.stringify(name === \"position\" ? position : value)}`\n            );\n            const comment = document.createComment(\n                ` From file: ${url} ; ${attributes.join(\" ; \")} `\n            );\n            if (position === \"attributes\") {\n                target.before(comment); // comment won't be visible if target is root\n            } else {\n                operation.prepend(comment);\n            }\n        }\n\n        switch (position) {\n            case \"replace\": {\n                root = replace(root, target, operation); // root can be replaced (see outer mode)\n                break;\n            }\n            case \"attributes\": {\n                modifyAttributes(target, operation);\n                break;\n            }\n            case \"inside\": {\n                const sentinel = document.createElement(\"sentinel\");\n                target.append(sentinel);\n                addBefore(sentinel, operation);\n                removeNode(sentinel);\n                break;\n            }\n            case \"after\": {\n                const sentinel = document.createElement(\"sentinel\");\n                target.after(sentinel);\n                addBefore(sentinel, operation);\n                removeNode(sentinel);\n                break;\n            }\n            case \"before\": {\n                addBefore(target, operation);\n                break;\n            }\n            default:\n                throw new Error(`Invalid position attribute: '${position}'`);\n        }\n    }\n    return root;\n}\n", "import { applyInheritance } from \"@web/core/template_inheritance\";\n\nconst parser = new DOMParser();\n/** @type {((document: Document) => void)[]} */\nconst templateProcessors = [];\n/** @type {((url: string) => boolean)[]} */\nlet urlFilters = [];\nfunction getParsedTemplate(templateString) {\n    const doc = parser.parseFromString(templateString, \"text/xml\");\n    for (const processor of templateProcessors) {\n        processor(doc);\n    }\n    return doc.firstChild;\n}\n\nfunction getClone(template) {\n    const c = template.cloneNode(true);\n    new Document().append(c); // => c is the documentElement of its ownerDocument\n    return c;\n}\n\nconst registered = new Set();\nfunction isRegistered(...args) {\n    const key = JSON.stringify([...args]);\n    if (registered.has(key)) {\n        return true;\n    }\n    registered.add(key);\n    return false;\n}\n\nlet blockType = null;\nlet blockId = 0;\n\nconst templates = {};\nconst parsedTemplates = {};\nconst info = {};\nexport function registerTemplate(name, url, templateString) {\n    if (isRegistered(...arguments)) {\n        return;\n    }\n    if (blockType !== \"templates\") {\n        blockType = \"templates\";\n        blockId++;\n    }\n    if (name in templates && (info[name].url !== url || templates[name] !== templateString)) {\n        throw new Error(`Template ${name} already exists`);\n    }\n    templates[name] = templateString;\n    info[name] = { blockId, url };\n}\n\nconst templateExtensions = {};\nconst parsedTemplateExtensions = {};\nexport function registerTemplateExtension(inheritFrom, url, templateString) {\n    if (isRegistered(...arguments)) {\n        return;\n    }\n    if (blockType !== \"extensions\") {\n        blockType = \"extensions\";\n        blockId++;\n    }\n    if (!templateExtensions[inheritFrom]) {\n        templateExtensions[inheritFrom] = [];\n    }\n    if (!templateExtensions[inheritFrom][blockId]) {\n        templateExtensions[inheritFrom][blockId] = [];\n    }\n    templateExtensions[inheritFrom][blockId].push({\n        templateString,\n        url,\n    });\n}\n\n/**\n * @param {(document: Document) => void} processor\n */\nexport function registerTemplateProcessor(processor) {\n    templateProcessors.push(processor);\n}\n\n/**\n * @param {typeof urlFilters} filters\n */\nexport function setUrlFilters(filters) {\n    urlFilters = filters;\n}\n\nfunction _getTemplate(name, blockId = null) {\n    if (!(name in parsedTemplates)) {\n        if (!(name in templates)) {\n            return null;\n        }\n        const templateString = templates[name];\n        parsedTemplates[name] = getParsedTemplate(templateString);\n    }\n    let processedTemplate = parsedTemplates[name];\n\n    const inheritFrom = processedTemplate.getAttribute(\"t-inherit\");\n    if (inheritFrom) {\n        const parentTemplate = _getTemplate(inheritFrom, blockId || info[name].blockId);\n        if (!parentTemplate) {\n            throw new Error(\n                `Constructing template ${name}: template parent ${inheritFrom} not found`\n            );\n        }\n        const element = getClone(processedTemplate);\n        processedTemplate = applyInheritance(getClone(parentTemplate), element, info[name].url);\n        if (processedTemplate.tagName !== element.tagName) {\n            const temp = processedTemplate;\n            processedTemplate = new Document().createElement(element.tagName);\n            processedTemplate.append(...temp.childNodes);\n        }\n        for (const { name, value } of element.attributes) {\n            if (![\"t-inherit\", \"t-inherit-mode\"].includes(name)) {\n                processedTemplate.setAttribute(name, value);\n            }\n        }\n    }\n\n    for (const otherBlockId in templateExtensions[name] || {}) {\n        if (blockId && otherBlockId > blockId) {\n            break;\n        }\n        if (!(name in parsedTemplateExtensions)) {\n            parsedTemplateExtensions[name] = {};\n        }\n        if (!(otherBlockId in parsedTemplateExtensions[name])) {\n            parsedTemplateExtensions[name][otherBlockId] = [];\n            for (const { templateString, url } of templateExtensions[name][otherBlockId]) {\n                parsedTemplateExtensions[name][otherBlockId].push({\n                    template: getParsedTemplate(templateString),\n                    url,\n                });\n            }\n        }\n        for (const { template, url } of parsedTemplateExtensions[name][otherBlockId]) {\n            if (!urlFilters.every((filter) => filter(url))) {\n                continue;\n            }\n            processedTemplate = applyInheritance(\n                inheritFrom ? processedTemplate : getClone(processedTemplate),\n                getClone(template),\n                url\n            );\n        }\n    }\n\n    return processedTemplate;\n}\n\n/** @type {Record<string, Element>} */\nlet processedTemplates = {};\n\n/**\n * @param {string} name\n */\nexport function getTemplate(name) {\n    if (!processedTemplates[name]) {\n        processedTemplates[name] = _getTemplate(name);\n    }\n    return processedTemplates[name];\n}\n\nexport function clearProcessedTemplates() {\n    processedTemplates = {};\n}\n\nexport function checkPrimaryTemplateParents(namesToCheck) {\n    const missing = new Set(namesToCheck.filter((name) => !(name in templates)));\n    if (missing.size) {\n        console.error(`Missing (primary) parent templates: ${[...missing].join(\", \")}`);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class Tooltip extends Component {\n    static template = \"web.Tooltip\";\n    static props = {\n        close: Function,\n        tooltip: { type: String, optional: true },\n        template: { type: String, optional: true },\n        info: { optional: true },\n    };\n}\n", "import { useService } from \"@web/core/utils/hooks\";\n\nimport { useEffect, useRef } from \"@odoo/owl\";\n\nexport function useTooltip(refName, params) {\n    const tooltip = useService(\"tooltip\");\n    const ref = useRef(refName);\n    useEffect(\n        (el) => tooltip.add(el, params),\n        () => [ref.el]\n    );\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { Tooltip } from \"./tooltip\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\n\nimport { whenReady } from \"@odoo/owl\";\n\n/**\n * The tooltip service allows to display custom tooltips on every elements with\n * a \"data-tooltip\" attribute. This attribute can be set on elements for which\n * we prefer a custom tooltip instead of the native one displaying the value of\n * the \"title\" attribute.\n *\n * Usage:\n *   <button data-tooltip=\"This is a tooltip\">Do something</button>\n *\n * The ideal position of the tooltip can be specified thanks to the attribute\n * \"data-tooltip-position\":\n *   <button data-tooltip=\"This is a tooltip\" data-tooltip-position=\"left\">Do something</button>\n *\n * The opening delay can be modified with the \"data-tooltip-delay\" attribute (default: 400):\n *   <button data-tooltip=\"This is a tooltip\" data-tooltip-delay=\"0\">Do something</button>\n *\n * The default behaviour on touch devices to open the tooltip can be modified from \"hold-to-show\"\n * to \"tap-to-show\" \"with the data-tooltip-touch-tap-to-show\" attribute:\n *  <button data-tooltip=\"This is a tooltip\" data-tooltip-touch-tap-to-show=\"true\">Do something</button>\n *\n * For advanced tooltips containing dynamic and/or html content, the\n * \"data-tooltip-template\" and \"data-tooltip-info\" attributes can be used.\n * For example, let's suppose the following qweb template:\n *   <t t-name=\"some_template\">\n *     <ul>\n *       <li>info.x</li>\n *       <li>info.y</li>\n *     </ul>\n *   </t>\n * This template can then be used in a tooltip as follows:\n *   <button data-tooltip-template=\"some_template\" data-tooltip-info=\"info\">Do something</button>\n * with \"info\" being a stringified object with two keys \"x\" and \"y\".\n */\n\nconst OPEN_DELAY = 400;\nconst CLOSE_DELAY = 200;\n\nexport const tooltipService = {\n    dependencies: [\"popover\"],\n    start(env, { popover }) {\n        let openTooltipTimeout;\n        let closeTooltip;\n        let target = null;\n        let touchPressed;\n        let mouseEntered;\n        const elementsWithTooltips = new Map();\n\n        /**\n         * Closes the currently opened tooltip if any, or prevent it from opening.\n         */\n        function cleanup() {\n            browser.clearTimeout(openTooltipTimeout);\n            if (closeTooltip) {\n                closeTooltip();\n            }\n        }\n\n        /**\n         * Checks that the target is in the DOM and we're hovering the target.\n         * @returns {boolean}\n         */\n        function shouldCleanup() {\n            if (!target) {\n                return false;\n            }\n            if (!document.body.contains(target)) {\n                return true; // target is no longer in the DOM\n            }\n            if (hasTouch() && !mouseEntered) {\n                return !touchPressed;\n            }\n            return false;\n        }\n\n        /**\n         * Checks whether there is a tooltip registered on the event target, and\n         * if there is, creates a timeout to open the corresponding tooltip\n         * after a delay.\n         *\n         * @param {HTMLElement} el the element on which to add the tooltip\n         * @param {object} param1\n         * @param {string} [param1.tooltip] the string to add as a tooltip, if\n         *  no tooltip template is specified\n         * @param {string} [param1.template] the name of the template to use for\n         *  tooltip, if any\n         * @param {object} [param1.info] info for the tooltip template\n         * @param {'top'|'bottom'|'left'|'right'} param1.position\n         * @param {number} [param1.delay] delay after which the popover should\n         *  open\n         */\n        function openTooltip(el, { tooltip = \"\", template, info, position, delay = OPEN_DELAY }) {\n            target = el;\n            cleanup();\n            if (!tooltip && !template) {\n                return;\n            }\n\n            openTooltipTimeout = browser.setTimeout(() => {\n                // verify that the element is still in the DOM\n                if (target.isConnected) {\n                    closeTooltip = popover.add(\n                        target,\n                        Tooltip,\n                        { tooltip, template, info },\n                        { position }\n                    );\n                    // Prevent title from showing on a parent at the same time\n                    target.title = \"\";\n                }\n            }, delay);\n        }\n\n        /**\n         * Checks whether there is a tooltip registered on the element, and\n         * if there is, creates a timeout to open the corresponding tooltip\n         * after a delay.\n         *\n         * @param {HTMLElement} el\n         */\n        function openElementsTooltip(el) {\n            // Fix weird behavior in Firefox where MouseEvent can be dispatched\n            // from TEXT_NODE, even if they shouldn't...\n            if (el.nodeType === Node.TEXT_NODE) {\n                return;\n            }\n            if (elementsWithTooltips.has(el)) {\n                openTooltip(el, elementsWithTooltips.get(el));\n            } else if (el.matches(\"[data-tooltip], [data-tooltip-template]\")) {\n                const dataset = el.dataset;\n                const params = {\n                    tooltip: dataset.tooltip,\n                    template: dataset.tooltipTemplate,\n                    position: dataset.tooltipPosition,\n                };\n                if (dataset.tooltipInfo) {\n                    params.info = JSON.parse(dataset.tooltipInfo);\n                }\n                if (dataset.tooltipDelay) {\n                    params.delay = parseInt(dataset.tooltipDelay, 10);\n                }\n                openTooltip(el, params);\n            }\n        }\n\n        /**\n         * Checks whether there is a tooltip registered on the event target, and\n         * if there is, creates a timeout to open the corresponding tooltip\n         * after a delay.\n         *\n         * @param {MouseEvent} ev a \"mouseenter\" event\n         */\n        function onMouseenter(ev) {\n            mouseEntered = true;\n            openElementsTooltip(ev.target);\n        }\n\n        function onMouseleave(ev) {\n            if (target === ev.target) {\n                mouseEntered = false;\n                cleanup();\n            }\n        }\n        /**\n         * Checks whether there is a tooltip registered on the event target, and\n         * if there is, creates a timeout to open the corresponding tooltip\n         * after a delay.\n         *\n         * @param {TouchEvent} ev a \"touchstart\" event\n         */\n        function onTouchStart(ev) {\n            touchPressed = true;\n            openElementsTooltip(ev.target);\n        }\n\n        whenReady(() => {\n            // Regularly check that the target is still in the DOM and if not, close the tooltip\n            browser.setInterval(() => {\n                if (shouldCleanup()) {\n                    cleanup();\n                }\n            }, CLOSE_DELAY);\n\n            if (hasTouch()) {\n                document.body.addEventListener(\"touchstart\", onTouchStart);\n\n                document.body.addEventListener(\"touchend\", (ev) => {\n                    if (ev.target.matches(\"[data-tooltip], [data-tooltip-template]\")) {\n                        if (!ev.target.dataset.tooltipTouchTapToShow) {\n                            touchPressed = false;\n                        }\n                    }\n                });\n\n                document.body.addEventListener(\"touchcancel\", (ev) => {\n                    if (ev.target.matches(\"[data-tooltip], [data-tooltip-template]\")) {\n                        if (!ev.target.dataset.tooltipTouchTapToShow) {\n                            touchPressed = false;\n                        }\n                    }\n                });\n            }\n\n            // Listen (using event delegation) to \"mouseenter\" events to open the tooltip if any\n            document.body.addEventListener(\"mouseenter\", onMouseenter, { capture: true });\n            // Listen (using event delegation) to \"mouseleave\" events to close the tooltip if any\n            document.body.addEventListener(\"mouseleave\", onMouseleave, { capture: true });\n        });\n\n        return {\n            add(el, params) {\n                elementsWithTooltips.set(el, params);\n                return () => {\n                    elementsWithTooltips.delete(el);\n                    if (target === el) {\n                        cleanup();\n                    }\n                };\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"tooltip\", tooltipService);\n", "import { browser } from \"./browser/browser\";\n\nimport {\n    Component,\n    onWillUpdateProps,\n    status,\n    useComponent,\n    useEffect,\n    useState,\n    xml,\n} from \"@odoo/owl\";\n\n// Allows to disable transitions globally, useful for testing (and maybe for\n// a reduced motion setting in the future?)\nexport const config = {\n    disabled: false,\n};\n/**\n * Creates a transition to be used within the current component. Usage:\n *  --- in JS:\n *  this.transition = useTransition({ name: \"myClass\" });\n *  --- in XML:\n *  <div t-if=\"transition.shouldMount\" t-att-class=\"transition.class\"/>\n *\n * @param {Object} options\n * @param {string} options.name the prefix to use for the transition classes\n * @param {boolean} [options.initialVisibility=true] whether to start the\n *  transition in the on or off state\n * @param {number} [options.immediate=false] (only relevant when initialVisibility\n *  is true) set to true to animate initially. By default, there's no animation\n *  if the element is initially visible.\n * @param {number} [options.leaveDuration] the leaveDuration of the transition\n * @param {Function} [options.onLeave] a function that will be called when the\n *  element will be removed in the next render cycle\n * @returns {{ shouldMount, class }} an object containing two fields that\n *  indicate whether an element on which the transition is applied should be\n *  mounted and the class string that should be put on it\n */\nexport function useTransition({\n    name,\n    initialVisibility = true,\n    immediate = false,\n    leaveDuration = 500,\n    onLeave = () => {},\n}) {\n    const component = useComponent();\n    const state = useState({\n        shouldMount: initialVisibility,\n        stage: initialVisibility ? \"enter\" : \"leave\",\n    });\n\n    if (config.disabled) {\n        return {\n            get shouldMount() {\n                return state.shouldMount;\n            },\n            set shouldMount(val) {\n                state.shouldMount = val;\n            },\n            get className() {\n                return `${name} ${name}-enter-active`;\n            },\n            get stage() {\n                return \"enter-active\";\n            },\n        };\n    }\n    // We need to allow the element to be mounted in the enter state so that it\n    // will get the transition when we activate the enter-active class. This\n    // onNextPatch allows us to activate the class that we want the next time\n    // the component is patched.\n    let onNextPatch = null;\n    useEffect(() => {\n        if (onNextPatch) {\n            onNextPatch();\n            onNextPatch = null;\n        }\n    });\n\n    let prevState, timer;\n    const transition = {\n        get shouldMount() {\n            return state.shouldMount;\n        },\n        set shouldMount(newState) {\n            if (newState === prevState) {\n                return;\n            }\n            browser.clearTimeout(timer);\n            prevState = newState;\n            // when true - transition from enter to enter-active\n            // when false - transition from enter-active to leave, unmount after leaveDuration\n            if (newState) {\n                if (status(component) === \"mounted\" || immediate) {\n                    state.stage = \"enter\";\n                    // force a render here so that we get a patch even if the state didn't change\n                    component.render();\n                    onNextPatch = () => {\n                        state.stage = \"enter-active\";\n                    };\n                } else {\n                    state.stage = \"enter-active\";\n                }\n                state.shouldMount = true;\n            } else {\n                state.stage = \"leave\";\n                timer = browser.setTimeout(() => {\n                    state.shouldMount = false;\n                    onLeave();\n                }, leaveDuration);\n            }\n        },\n        get className() {\n            return `${name} ${name}-${state.stage}`;\n        },\n        get stage() {\n            return state.stage;\n        },\n    };\n    transition.shouldMount = initialVisibility;\n    return transition;\n}\n\n/**\n * A higher order component that handles a transition to be used within its\n * default slot. Generally, the useTransition hook is simpler to use, but the\n * HOC has the advantage that it can be spawned as needed during the render (eg:\n * in a t-foreach loop) without knowing at setup-time how many transitions need\n * to be created. @see useTransition\n */\nexport class Transition extends Component {\n    static template = xml`<t t-slot=\"default\" t-if=\"transition.shouldMount\" className=\"transition.className\"/>`;\n    static props = {\n        name: String,\n        visible: { type: Boolean, optional: true },\n        immediate: { type: Boolean, optional: true },\n        leaveDuration: { type: Number, optional: true },\n        onLeave: { type: Function, optional: true },\n        slots: Object,\n    };\n\n    setup() {\n        const { immediate, visible, leaveDuration, name, onLeave } = this.props;\n        this.transition = useTransition({\n            initialVisibility: visible,\n            immediate,\n            leaveDuration,\n            name,\n            onLeave,\n        });\n        onWillUpdateProps(({ visible = true }) => {\n            this.transition.shouldMount = visible;\n        });\n    }\n}\n", "import { Domain } from \"@web/core/domain\";\nimport { formatAST, parseExpr } from \"@web/core/py_js/py\";\nimport { toPyValue } from \"@web/core/py_js/py_utils\";\nimport { deepCopy, deepEqual } from \"../utils/objects\";\n\n/** @typedef { import(\"@web/core/py_js/py_parser\").AST } AST */\n/** @typedef {import(\"@web/core/domain\").DomainRepr} DomainRepr */\n\n/**\n * @typedef {number|string|boolean|Expression} Atom\n */\n\n/**\n * @typedef {Atom|Atom[]} Value\n */\n\n/**\n * @typedef {Object} Condition\n * @property {\"condition\"} type\n * @property {Value} path\n * @property {Value} operator\n * @property {Value} value\n * @property {boolean} negate\n */\n\n/**\n * @typedef {Object} ComplexCondition\n * @property {\"complex_condition\"} type\n * @property {string} value expression\n */\n\n/**\n * @typedef {Object} Connector\n * @property {\"connector\"} type\n * @property {boolean} negate\n * @property {\"|\"|\"&\"} value\n * @property {Tree[]} children\n */\n\n/**\n * @typedef {Connector|Condition|ComplexCondition} Tree\n */\n\n/**\n * @typedef {Object} Options\n * @property {(value: Value) => (null|Object)} [getFieldDef]\n * @property {boolean} [distributeNot]\n */\n\nexport const TERM_OPERATORS_NEGATION = {\n    \"<\": \">=\",\n    \">\": \"<=\",\n    \"<=\": \">\",\n    \">=\": \"<\",\n    \"=\": \"!=\",\n    \"!=\": \"=\",\n    in: \"not in\",\n    like: \"not like\",\n    ilike: \"not ilike\",\n    \"not in\": \"in\",\n    \"not like\": \"like\",\n    \"not ilike\": \"ilike\",\n};\n\nconst TERM_OPERATORS_NEGATION_EXTENDED = {\n    ...TERM_OPERATORS_NEGATION,\n    is: \"is not\",\n    \"is not\": \"is\",\n    \"==\": \"!=\",\n    \"!=\": \"==\", // override here\n};\n\nconst EXCHANGE = {\n    \"<\": \">\",\n    \"<=\": \">=\",\n    \">\": \"<\",\n    \">=\": \"<=\",\n    \"=\": \"=\",\n    \"!=\": \"!=\",\n};\n\nconst COMPARATORS = [\"<\", \"<=\", \">\", \">=\", \"in\", \"not in\", \"==\", \"is\", \"!=\", \"is not\"];\n\nconst DATETIME_TODAY_STRING_EXPRESSION = `datetime.datetime.combine(context_today(), datetime.time(0, 0, 0)).to_utc().strftime(\"%Y-%m-%d %H:%M:%S\")`;\nconst DATE_TODAY_STRING_EXPRESSION = `context_today().strftime(\"%Y-%m-%d\")`;\nconst DELTA_DATE_AST = parseExpr(\n    `(context_today() + relativedelta(period=amount)).strftime('%Y-%m-%d')`\n);\nconst DELTA_DATETIME_AST = parseExpr(\n    `datetime.datetime.combine(context_today() + relativedelta(period=amount), datetime.time(0, 0, 0)).to_utc().strftime(\"%Y-%m-%d %H:%M:%S\")`\n);\n\nfunction replaceKwargs(ast, fieldType, kwargs = {}) {\n    const astCopy = deepCopy(ast);\n    if (fieldType === \"date\") {\n        astCopy.fn.obj.right.kwargs = kwargs;\n    } else {\n        astCopy.fn.obj.fn.obj.args[0].right.kwargs = kwargs;\n    }\n    return astCopy;\n}\n\nfunction getDelta(ast, fieldType) {\n    const kwargs =\n        (fieldType === \"date\"\n            ? ast.fn?.obj?.right?.kwargs\n            : ast.fn?.obj?.fn?.obj?.args?.[0]?.right?.kwargs) || {};\n    if (Object.keys(kwargs).length !== 1) {\n        return null;\n    }\n    if (\n        !deepEqual(\n            replaceKwargs(ast, fieldType),\n            replaceKwargs(fieldType === \"date\" ? DELTA_DATE_AST : DELTA_DATETIME_AST, fieldType)\n        )\n    ) {\n        return null;\n    }\n    const [option, amountAST] = Object.entries(kwargs)[0];\n    return [toValue(amountAST), option];\n}\n\nfunction getDeltaExpression(value, fieldType) {\n    const ast = replaceKwargs(\n        fieldType === \"date\" ? DELTA_DATE_AST : DELTA_DATETIME_AST,\n        fieldType,\n        { [value[1]]: toAST(value[0]) }\n    );\n    return expression(formatAST(ast));\n}\n\nfunction isTodayExpr(val, type) {\n    return (\n        val._expr ===\n        (type === \"date\" ? DATE_TODAY_STRING_EXPRESSION : DATETIME_TODAY_STRING_EXPRESSION)\n    );\n}\n\nexport class Expression {\n    constructor(ast) {\n        if (typeof ast === \"string\") {\n            ast = parseExpr(ast);\n        }\n        this._ast = ast;\n        this._expr = formatAST(ast);\n    }\n\n    toAST() {\n        return this._ast;\n    }\n\n    toString() {\n        return this._expr;\n    }\n}\n\n/**\n * @param {string} expr\n * @returns {Expression}\n */\nexport function expression(expr) {\n    return new Expression(expr);\n}\n\n/**\n * @param {\"|\"|\"&\"} value\n * @param {Tree[]} [children=[]]\n * @param {boolean} [negate=false]\n * @returns {Connector}\n */\nexport function connector(value, children = [], negate = false) {\n    return { type: \"connector\", value, children, negate };\n}\n\n/**\n * @param {Value} path\n * @param {Value} operator\n * @param {Value} value\n * @param {boolean} [negate=false]\n * @returns {Condition}\n */\nexport function condition(path, operator, value, negate = false) {\n    return { type: \"condition\", path, operator, value, negate };\n}\n\n/**\n * @param {string} value\n * @returns {ComplexCondition}\n */\nexport function complexCondition(value) {\n    parseExpr(value);\n    return { type: \"complex_condition\", value };\n}\n\n/**\n * @param {Value} value\n * @returns {Value}\n */\nfunction cloneValue(value) {\n    if (value instanceof Expression) {\n        return new Expression(value.toAST());\n    }\n    if (Array.isArray(value)) {\n        return value.map(cloneValue);\n    }\n    return value;\n}\n\n/**\n * @param {Tree} tree\n * @returns {Tree}\n */\nexport function cloneTree(tree) {\n    const clone = {};\n    for (const key in tree) {\n        clone[key] = cloneValue(tree[key]);\n    }\n    return clone;\n}\n\nexport function formatValue(value) {\n    return formatAST(toAST(value));\n}\n\nexport function normalizeValue(value) {\n    return toValue(toAST(value)); // no array in array (see isWithinArray)\n}\n\n/**\n * @param {import(\"@web/core/py_js/py_parser\").AST} ast\n * @returns {Value}\n */\nexport function toValue(ast, isWithinArray = false) {\n    if ([4, 10].includes(ast.type) && !isWithinArray) {\n        /** 4: list, 10: tuple */\n        return ast.value.map((v) => toValue(v, true));\n    } else if ([0, 1, 2].includes(ast.type)) {\n        /** 0: number, 1: string, 2: boolean */\n        return ast.value;\n    } else if (ast.type === 6 && ast.op === \"-\" && ast.right.type === 0) {\n        /** 6: unary operator */\n        return -ast.right.value;\n    } else if (ast.type === 5 && [\"false\", \"true\"].includes(ast.value)) {\n        /** 5: name */\n        return JSON.parse(ast.value);\n    } else {\n        return new Expression(ast);\n    }\n}\n\nexport function isTree(value) {\n    return (\n        typeof value === \"object\" &&\n        !(value instanceof Domain) &&\n        !(value instanceof Expression) &&\n        !Array.isArray(value) &&\n        value !== null\n    );\n}\n\n/**\n * @param {Value} value\n * @returns  {import(\"@web/core/py_js/py_parser\").AST}\n */\nfunction toAST(value) {\n    if (isTree(value)) {\n        const domain = new Domain(domainFromTree(value));\n        return domain.ast;\n    }\n    if (value instanceof Expression) {\n        return value.toAST();\n    }\n    if (Array.isArray(value)) {\n        return { type: 4, value: value.map(toAST) };\n    }\n    return toPyValue(value);\n}\n\n/**\n * @param {AND|OR} parent\n * @param {Tree} child\n */\nfunction addChild(parent, child) {\n    if (child.type === \"connector\" && !child.negate && child.value === parent.value) {\n        parent.children.push(...child.children);\n    } else {\n        parent.children.push(child);\n    }\n}\n\n/**\n * @param {Condition} condition\n * @returns {Condition}\n */\nfunction getNormalizedCondition(condition) {\n    let { operator, negate } = condition;\n    if (negate && typeof operator === \"string\" && TERM_OPERATORS_NEGATION[operator]) {\n        operator = TERM_OPERATORS_NEGATION[operator];\n        negate = false;\n    }\n    return { ...condition, operator, negate };\n}\n\nfunction normalizeCondition(condition) {\n    Object.assign(condition, getNormalizedCondition(condition));\n}\n\n/**\n * @param {AST[]} ASTs\n * @param {boolean} distributeNot\n * @param {boolean} [negate=false]\n * @returns {{ tree: Tree, remaimingASTs: AST[] }}\n */\nfunction _construcTree(ASTs, distributeNot, negate = false) {\n    const [firstAST, ...tailASTs] = ASTs;\n\n    if (firstAST.type === 1 && firstAST.value === \"!\") {\n        return _construcTree(tailASTs, distributeNot, !negate);\n    }\n\n    const tree = { type: firstAST.type === 1 ? \"connector\" : \"condition\" };\n    if (tree.type === \"connector\") {\n        tree.value = firstAST.value;\n        if (distributeNot && negate) {\n            tree.value = tree.value === \"&\" ? \"|\" : \"&\";\n            tree.negate = false;\n        } else {\n            tree.negate = negate;\n        }\n        tree.children = [];\n    } else {\n        const [pathAST, operatorAST, valueAST] = firstAST.value;\n        tree.path = toValue(pathAST);\n        tree.negate = negate;\n        tree.operator = toValue(operatorAST);\n        tree.value = toValue(valueAST);\n        if ([\"any\", \"not any\"].includes(tree.operator)) {\n            try {\n                tree.value = treeFromDomain(formatAST(valueAST));\n            } catch {\n                tree.value = Array.isArray(tree.value) ? tree.value : [tree.value];\n            }\n        }\n        normalizeCondition(tree);\n    }\n    let remaimingASTs = tailASTs;\n    if (tree.type === \"connector\") {\n        for (let i = 0; i < 2; i++) {\n            const { tree: child, remaimingASTs: otherASTs } = _construcTree(\n                remaimingASTs,\n                distributeNot,\n                distributeNot && negate\n            );\n            remaimingASTs = otherASTs;\n            addChild(tree, child);\n        }\n    }\n    return { tree, remaimingASTs };\n}\n\n/**\n * @param {AST[]} initialASTs\n * @param {Object} options\n * @param {boolean} [options.distributeNot=false]\n * @returns {Tree}\n */\nfunction construcTree(initialASTs, options) {\n    if (!initialASTs.length) {\n        return connector(\"&\");\n    }\n    const { tree } = _construcTree(initialASTs, options.distributeNot);\n    return tree;\n}\n\n/**\n * @param {Tree} tree\n * @returns {AST[]}\n */\nfunction getASTs(tree) {\n    const ASTs = [];\n    if (tree.type === \"condition\") {\n        if (tree.negate) {\n            ASTs.push(toAST(\"!\"));\n        }\n        ASTs.push({\n            type: 10,\n            value: [tree.path, tree.operator, tree.value].map(toAST),\n        });\n        return ASTs;\n    }\n\n    const length = tree.children.length;\n    if (length && tree.negate) {\n        ASTs.push(toAST(\"!\"));\n    }\n    for (let i = 0; i < length - 1; i++) {\n        ASTs.push(toAST(tree.value));\n    }\n    for (const child of tree.children) {\n        ASTs.push(...getASTs(child));\n    }\n    return ASTs;\n}\n\nfunction not(ast) {\n    if (isNot(ast)) {\n        return ast.right;\n    }\n    if (ast.type === 2) {\n        return { ...ast, value: !ast.value };\n    }\n    if (ast.type === 7 && COMPARATORS.includes(ast.op)) {\n        return { ...ast, op: TERM_OPERATORS_NEGATION_EXTENDED[ast.op] }; // do not use this if ast is within a domain context!\n    }\n    return { type: 6, op: \"not\", right: isBool(ast) ? ast.args[0] : ast };\n}\n\nfunction bool(ast) {\n    if (isBool(ast) || isNot(ast) || ast.type === 2) {\n        return ast;\n    }\n    return { type: 8, fn: { type: 5, value: \"bool\" }, args: [ast], kwargs: {} };\n}\n\nfunction name(value) {\n    return { type: 5, value };\n}\n\nfunction or(left, right) {\n    return { type: 14, op: \"or\", left, right };\n}\n\nfunction and(left, right) {\n    return { type: 14, op: \"and\", left, right };\n}\n\nfunction isNot(ast) {\n    return ast.type === 6 && ast.op === \"not\";\n}\n\nfunction is(oneParamFunc, ast) {\n    return (\n        ast.type === 8 &&\n        ast.fn.type === 5 &&\n        ast.fn.value === oneParamFunc &&\n        ast.args.length === 1\n    ); // improve condition?\n}\n\nfunction isSet(ast) {\n    return ast.type === 8 && ast.fn.type === 5 && ast.fn.value === \"set\" && ast.args.length <= 1;\n}\n\nfunction isBool(ast) {\n    return is(\"bool\", ast);\n}\n\nfunction isValidPath(ast, options) {\n    const getFieldDef = options.getFieldDef || (() => null);\n    if (ast.type === 5) {\n        return getFieldDef(ast.value) !== null;\n    }\n    return false;\n}\n\nfunction isX2Many(ast, options) {\n    if (isValidPath(ast, options)) {\n        const fieldDef = options.getFieldDef(ast.value); // safe: isValidPath has not returned null;\n        return [\"many2many\", \"one2many\"].includes(fieldDef.type);\n    }\n    return false;\n}\n\nfunction _getConditionFromComparator(ast, options) {\n    if ([\"is\", \"is not\"].includes(ast.op)) {\n        // we could do something smarter here\n        // e.g. if left is a boolean field and right is a boolean\n        // we can create a condition based on \"=\"\n        return null;\n    }\n\n    let operator = ast.op;\n    if (operator === \"==\") {\n        operator = \"=\";\n    }\n\n    let left = ast.left;\n    let right = ast.right;\n    if (isValidPath(left, options) == isValidPath(right, options)) {\n        return null;\n    }\n\n    if (!isValidPath(left, options)) {\n        if (operator in EXCHANGE) {\n            const temp = left;\n            left = right;\n            right = temp;\n            operator = EXCHANGE[operator];\n        } else {\n            return null;\n        }\n    }\n\n    return condition(left.value, operator, toValue(right));\n}\n\nfunction isValidPath2(ast, options) {\n    if (!ast) {\n        return null;\n    }\n    if ([4, 10].includes(ast.type) && ast.value.length === 1) {\n        return isValidPath(ast.value[0], options);\n    }\n    return isValidPath(ast, options);\n}\n\nfunction _getConditionFromIntersection(ast, options, negate = false) {\n    let left = ast.fn.obj.args[0];\n    let right = ast.args[0];\n\n    if (!left) {\n        return condition(negate ? 1 : 0, \"=\", 1);\n    }\n\n    // left/right exchange\n    if (isValidPath2(left, options) == isValidPath2(right, options)) {\n        return null;\n    }\n    if (!isValidPath2(left, options)) {\n        const temp = left;\n        left = right;\n        right = temp;\n    }\n\n    if ([4, 10].includes(left.type) && left.value.length === 1) {\n        left = left.value[0];\n    }\n\n    if (!right) {\n        return condition(left.value, negate ? \"=\" : \"!=\", false);\n    }\n\n    // try to extract the ast of an iterable\n    // we only make simple conversions here\n    if (isSet(right)) {\n        if (!right.args[0]) {\n            right = { type: 4, value: [] };\n        }\n        if ([4, 10].includes(right.args[0].type)) {\n            right = right.args[0];\n        }\n    }\n\n    if (![4, 10].includes(right.type)) {\n        return null;\n    }\n\n    return condition(left.value, negate ? \"not in\" : \"in\", toValue(right));\n}\n\n/**\n * @param {AST} ast\n * @param {Options} options\n * @param {boolean} [negate=false]\n * @returns {Condition|ComplexCondition}\n */\nfunction _leafFromAST(ast, options, negate = false) {\n    if (isNot(ast)) {\n        return _treeFromAST(ast.right, options, !negate);\n    }\n\n    if (ast.type === 5 /** name */ && isValidPath(ast, options)) {\n        return condition(ast.value, negate ? \"=\" : \"!=\", false);\n    }\n\n    const astValue = toValue(ast);\n    if ([\"boolean\", \"number\", \"string\"].includes(typeof astValue)) {\n        return condition(astValue ? 1 : 0, \"=\", 1);\n    }\n\n    if (\n        ast.type === 8 &&\n        ast.fn.type === 15 /** object lookup */ &&\n        isSet(ast.fn.obj) &&\n        ast.fn.key === \"intersection\"\n    ) {\n        const tree = _getConditionFromIntersection(ast, options, negate);\n        if (tree) {\n            return tree;\n        }\n    }\n\n    if (ast.type === 7 && COMPARATORS.includes(ast.op)) {\n        if (negate) {\n            return _leafFromAST(not(ast), options);\n        }\n        const tree = _getConditionFromComparator(ast, options);\n        if (tree) {\n            return tree;\n        }\n    }\n\n    // no conclusive/simple way to transform ast in a condition\n    return complexCondition(formatAST(negate ? not(ast) : ast));\n}\n\n/**\n * @param {AST} ast\n * @param {Options} options\n * @param {boolean} [negate=false]\n * @returns {Tree}\n */\nfunction _treeFromAST(ast, options, negate = false) {\n    if (isNot(ast)) {\n        return _treeFromAST(ast.right, options, !negate);\n    }\n\n    if (ast.type === 14) {\n        const tree = connector(\n            ast.op === \"and\" ? \"&\" : \"|\" // and/or are the only ops that are given type 14 (for now)\n        );\n        if (options.distributeNot && negate) {\n            tree.value = tree.value === \"&\" ? \"|\" : \"&\";\n        } else {\n            tree.negate = negate;\n        }\n        const subASTs = [ast.left, ast.right];\n        for (const subAST of subASTs) {\n            const child = _treeFromAST(subAST, options, options.distributeNot && negate);\n            addChild(tree, child);\n        }\n        return tree;\n    }\n\n    if (ast.type === 13) {\n        const newAST = or(and(ast.condition, ast.ifTrue), and(not(ast.condition), ast.ifFalse));\n        return _treeFromAST(newAST, options, negate);\n    }\n\n    return _leafFromAST(ast, options, negate);\n}\n\nfunction _expressionFromTree(tree, options, isRoot = false) {\n    if (tree.type === \"connector\" && tree.value === \"|\" && tree.children.length === 2) {\n        // check if we have an \"if else\"\n        const isSimpleAnd = (tree) =>\n            tree.type === \"connector\" && tree.value === \"&\" && tree.children.length === 2;\n        if (tree.children.every((c) => isSimpleAnd(c))) {\n            const [c1, c2] = tree.children;\n            for (let i = 0; i < 2; i++) {\n                const c1Child = c1.children[i];\n                const str1 = _expressionFromTree({ ...c1Child }, options);\n                for (let j = 0; j < 2; j++) {\n                    const c2Child = c2.children[j];\n                    const str2 = _expressionFromTree(c2Child, options);\n                    if (str1 === `not ${str2}` || `not ${str1}` === str2) {\n                        /** @todo smth smarter. this is very fragile */\n                        const others = [c1.children[1 - i], c2.children[1 - j]];\n                        const str = _expressionFromTree(c1Child, options);\n                        const strs = others.map((c) => _expressionFromTree(c, options));\n                        return `${strs[0]} if ${str} else ${strs[1]}`;\n                    }\n                }\n            }\n        }\n    }\n\n    if (tree.type === \"connector\") {\n        const connector = tree.value === \"&\" ? \"and\" : \"or\";\n        const subExpressions = tree.children.map((c) => _expressionFromTree(c, options));\n        if (!subExpressions.length) {\n            return connector === \"and\" ? \"1\" : \"0\";\n        }\n        let expression = subExpressions.join(` ${connector} `);\n        if (!isRoot || tree.negate) {\n            expression = `( ${expression} )`;\n        }\n        if (tree.negate) {\n            expression = `not ${expression}`;\n        }\n        return expression;\n    }\n\n    if (tree.type === \"complex_condition\") {\n        return tree.value;\n    }\n\n    tree = getNormalizedCondition(tree);\n    const { path, operator, value } = tree;\n\n    const op = operator === \"=\" ? \"==\" : operator; // do something about is ?\n    if (typeof op !== \"string\" || !COMPARATORS.includes(op)) {\n        throw new Error(\"Invalid operator\");\n    }\n\n    // we can assume that negate = false here: comparators have negation defined\n    // and the tree has been normalized\n\n    if ([0, 1].includes(path)) {\n        if (operator !== \"=\" || value !== 1) {\n            // check if this is too restricive for us\n            return new Error(\"Invalid condition\");\n        }\n        return formatAST({ type: 2, value: Boolean(path) });\n    }\n\n    const pathAST = toAST(path);\n    if (typeof path == \"string\" && isValidPath(name(path), options)) {\n        pathAST.type = 5;\n    }\n\n    if (value === false && [\"=\", \"!=\"].includes(operator)) {\n        // true makes sense for non boolean fields?\n        return formatAST(operator === \"=\" ? not(pathAST) : pathAST);\n    }\n\n    let valueAST = toAST(value);\n    if (\n        [\"in\", \"not in\"].includes(operator) &&\n        !(value instanceof Expression) &&\n        ![4, 10].includes(valueAST.type)\n    ) {\n        valueAST = { type: 4, value: [valueAST] };\n    }\n\n    if (pathAST.type === 5 && isX2Many(pathAST, options) && [\"in\", \"not in\"].includes(operator)) {\n        const ast = {\n            type: 8,\n            fn: {\n                type: 15,\n                obj: {\n                    args: [pathAST],\n                    type: 8,\n                    fn: {\n                        type: 5,\n                        value: \"set\",\n                    },\n                },\n                key: \"intersection\",\n            },\n            args: [valueAST],\n        };\n        return formatAST(operator === \"not in\" ? not(ast) : ast);\n    }\n\n    // add case true for boolean fields\n\n    return formatAST({\n        type: 7,\n        op,\n        left: pathAST,\n        right: valueAST,\n    });\n}\n\n////////////////////////////////////////////////////////////////////////////////\n//  PUBLIC: CREATE/REMOVE\n//    between operator\n//    is, is_not, set, not_set operators\n//    complex conditions\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * @param {Tree} tree\n * @returns {Tree}\n */\nfunction createBetweenOperators(tree) {\n    if ([\"condition\", \"complex_condition\"].includes(tree.type)) {\n        return tree;\n    }\n    const processedChildren = tree.children.map(createBetweenOperators);\n    if (tree.value === \"|\") {\n        return { ...tree, children: processedChildren };\n    }\n    const children = [];\n    for (let i = 0; i < processedChildren.length; i++) {\n        const child1 = processedChildren[i];\n        const child2 = processedChildren[i + 1];\n        if (\n            child1.type === \"condition\" &&\n            child2 &&\n            child2.type === \"condition\" &&\n            formatValue(child1.path) === formatValue(child2.path) &&\n            child1.operator === \">=\" &&\n            child2.operator === \"<=\"\n        ) {\n            children.push(\n                condition(child1.path, \"between\", normalizeValue([child1.value, child2.value]))\n            );\n            i += 1;\n        } else {\n            children.push(child1);\n        }\n    }\n    if (children.length === 1) {\n        return { ...children[0] };\n    }\n    return { ...tree, children };\n}\n\n/**\n * @param {Tree} tree\n * @param {Options} [options={}]\n * @returns {Tree}\n */\nfunction createWithinOperators(tree, options = {}) {\n    if (tree.children) {\n        return {\n            ...tree,\n            children: tree.children.map((child) => createWithinOperators(child, options)),\n        };\n    }\n    const fieldType = options.getFieldDef?.(tree.path)?.type;\n    if (tree.operator !== \"between\" || ![\"date\", \"datetime\"].includes(fieldType)) {\n        return tree;\n    }\n\n    function getProcessedDelta(val, periodShouldBePositive = true) {\n        const delta = getDelta(toAST(val), fieldType);\n        if (delta) {\n            const [amount] = delta;\n            if (\n                Number.isInteger(amount) &&\n                // @ts-ignore\n                ((amount < 0 && periodShouldBePositive) || (amount > 0 && !periodShouldBePositive))\n            ) {\n                return null;\n            }\n        }\n        return delta;\n    }\n\n    const newTree = { ...tree };\n\n    if (isTodayExpr(newTree.value[0], fieldType)) {\n        const delta = getProcessedDelta(newTree.value[1]);\n        if (delta) {\n            newTree.operator = \"within\";\n            newTree.value = [...delta, fieldType];\n        }\n    } else if (isTodayExpr(newTree.value[1], fieldType)) {\n        const delta = getProcessedDelta(newTree.value[0], false);\n        if (delta) {\n            newTree.operator = \"within\";\n            newTree.value = [...delta, fieldType];\n        }\n    }\n\n    return newTree;\n}\n\n/**\n * @param {Tree} tree\n * @returns {Tree}\n */\nexport function removeBetweenOperators(tree) {\n    if (tree.type === \"complex_condition\") {\n        return tree;\n    }\n    if (tree.type === \"condition\") {\n        if (tree.operator !== \"between\") {\n            return tree;\n        }\n        const { negate, path, value } = tree;\n        return connector(\n            \"&\",\n            [condition(path, \">=\", value[0]), condition(path, \"<=\", value[1])],\n            negate\n        );\n    }\n    const processedChildren = tree.children.map(removeBetweenOperators);\n    if (tree.value === \"|\") {\n        return { ...tree, children: processedChildren };\n    }\n    const newTree = { ...tree, children: [] };\n    // after processing a child might have become a connector \"&\" --> normalize\n    for (let i = 0; i < processedChildren.length; i++) {\n        addChild(newTree, processedChildren[i]);\n    }\n    return newTree;\n}\n\nexport function removeWithinOperators(tree) {\n    if (tree.type === \"complex_condition\") {\n        return tree;\n    }\n    if (tree.type === \"condition\") {\n        if (tree.operator !== \"within\") {\n            return tree;\n        }\n        const { negate, path, value } = tree;\n        const fieldType = value[2];\n        const expressions = [\n            expression(\n                fieldType === \"date\"\n                    ? DATE_TODAY_STRING_EXPRESSION\n                    : DATETIME_TODAY_STRING_EXPRESSION\n            ),\n            getDeltaExpression(value, fieldType),\n        ];\n        const reverse = Number.isInteger(value[0]) && value[0] > 0;\n        return condition(\n            path,\n            \"between\",\n            reverse ? Object.values(expressions) : Object.values(expressions).reverse(),\n            negate\n        );\n    }\n    const processedChildren = tree.children.map(removeWithinOperators);\n    return { ...tree, children: processedChildren };\n}\n\n/**\n * @param {Tree} tree\n * @param {options} [options={}]\n * @param {Function} [options.getFieldDef]\n * @returns {Tree}\n */\nexport function createVirtualOperators(tree, options = {}) {\n    if (tree.type === \"condition\") {\n        const { path, operator, value } = tree;\n        if ([\"=\", \"!=\"].includes(operator)) {\n            const fieldDef = options.getFieldDef?.(path) || null;\n            if (fieldDef) {\n                if (fieldDef.type === \"boolean\") {\n                    return { ...tree, operator: operator === \"=\" ? \"is\" : \"is_not\" };\n                } else if (\n                    ![\"many2one\", \"date\", \"datetime\"].includes(fieldDef?.type) &&\n                    value === false\n                ) {\n                    return { ...tree, operator: operator === \"=\" ? \"not_set\" : \"set\" };\n                }\n            }\n        }\n        if (operator === \"=ilike\") {\n            if (value.endsWith?.(\"%\")) {\n                return { ...tree, operator: \"starts_with\", value: value.slice(0, -1) };\n            }\n            if (value.startsWith?.(\"%\")) {\n                return { ...tree, operator: \"ends_with\", value: value.slice(1) };\n            }\n        }\n        return tree;\n    }\n    if (tree.type === \"complex_condition\") {\n        return tree;\n    }\n    const processedChildren = tree.children.map((c) => createVirtualOperators(c, options));\n    return { ...tree, children: processedChildren };\n}\n\n/**\n * @param {Tree} tree\n * @returns {Tree}\n */\nexport function removeVirtualOperators(tree) {\n    if (tree.type === \"condition\") {\n        const { operator, value } = tree;\n        if ([\"is\", \"is_not\"].includes(operator)) {\n            return { ...tree, operator: operator === \"is\" ? \"=\" : \"!=\" };\n        }\n        if ([\"set\", \"not_set\"].includes(operator)) {\n            return { ...tree, operator: operator === \"set\" ? \"!=\" : \"=\" };\n        }\n        if ([\"starts_with\", \"ends_with\"].includes(operator)) {\n            return {\n                ...tree,\n                value: operator === \"starts_with\" ? `${value}%` : `%${value}`,\n                operator: \"=ilike\",\n            };\n        }\n        return tree;\n    }\n    if (tree.type === \"complex_condition\") {\n        return tree;\n    }\n    const processedChildren = tree.children.map((c) => removeVirtualOperators(c));\n    return { ...tree, children: processedChildren };\n}\n\n/**\n * @param {Tree} tree\n * @returns {Tree} the conditions better expressed as complex conditions become complex conditions\n */\nfunction createComplexConditions(tree) {\n    if (tree.type === \"condition\") {\n        if (tree.path instanceof Expression && tree.operator === \"=\" && tree.value === 1) {\n            // not sure about this one -> we should maybe evaluate the condition and check\n            // if it does not become something e.g. the name of a integer field?\n            return complexCondition(String(tree.path));\n        }\n        return cloneTree(tree);\n    }\n    if (tree.type === \"complex_condition\") {\n        return cloneTree(tree);\n    }\n    return {\n        ...tree,\n        children: tree.children.map((child) => createComplexConditions(child)),\n    };\n}\n\n/**\n * @param {Tree} tree\n * @returns {Tree} a simple tree (without complex conditions)\n */\nfunction removeComplexConditions(tree) {\n    if (tree.type === \"condition\") {\n        return cloneTree(tree);\n    }\n    if (tree.type === \"complex_condition\") {\n        const ast = parseExpr(tree.value);\n        return condition(new Expression(bool(ast)), \"=\", 1);\n    }\n    return {\n        ...tree,\n        children: tree.children.map((child) => removeComplexConditions(child)),\n    };\n}\n\n////////////////////////////////////////////////////////////////////////////////\n//  PUBLIC: MAPPINGS\n//    tree <-> expression\n//    domain <-> expression\n//    expression <-> tree\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * @param {string} expression\n * @param {Options} [options={}]\n * @returns {Tree} a tree representation of an expression\n */\nexport function treeFromExpression(expression, options = {}) {\n    const ast = parseExpr(expression);\n    const tree = _treeFromAST(ast, options);\n    return createVirtualOperators(\n        createWithinOperators(createBetweenOperators(tree), options),\n        options\n    );\n}\n\n/**\n * @param {Tree} tree\n * @param {Options} [options={}]\n * @returns {string} an expression\n */\nexport function expressionFromTree(tree, options = {}) {\n    const simplifiedTree = createComplexConditions(\n        removeBetweenOperators(removeWithinOperators(removeVirtualOperators(tree)))\n    );\n    return _expressionFromTree(simplifiedTree, options, true);\n}\n\n/**\n * @param {Tree} tree\n * @returns {string} a string representation of a domain\n */\nexport function domainFromTree(tree) {\n    const simplifiedTree = removeBetweenOperators(\n        removeWithinOperators(removeVirtualOperators(removeComplexConditions(tree)))\n    );\n    const domainAST = {\n        type: 4,\n        value: getASTs(simplifiedTree),\n    };\n    return formatAST(domainAST);\n}\n\n/**\n * @param {DomainRepr} domain\n * @param {Object} [options={}] see construcTree API\n * @returns {Tree} a (simple) tree representation of a domain\n */\nexport function treeFromDomain(domain, options = {}) {\n    domain = new Domain(domain);\n    const domainAST = domain.ast;\n    const tree = construcTree(domainAST.value, options); // a simple tree\n    return createVirtualOperators(\n        createWithinOperators(createBetweenOperators(tree), options),\n        options\n    );\n}\n\n/**\n * @param {DomainRepr} domain a string representation of a domain\n * @param {Options} [options={}]\n * @returns {string} an expression\n */\nexport function expressionFromDomain(domain, options = {}) {\n    const tree = treeFromDomain(domain, options);\n    return expressionFromTree(tree, options);\n}\n\n/**\n * @param {string} expression an expression\n * @param {Options} [options={}]\n * @returns {string} a string representation of a domain\n */\nexport function domainFromExpression(expression, options = {}) {\n    const tree = treeFromExpression(expression, options);\n    return domainFromTree(tree);\n}\n", "import {\n    getResModel,\n    useMakeGetFieldDef,\n    useMakeGetConditionDescription,\n} from \"@web/core/tree_editor/utils\";\nimport { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport {\n    condition,\n    cloneTree,\n    formatValue,\n    removeVirtualOperators,\n    connector,\n    isTree,\n} from \"@web/core/tree_editor/condition_tree\";\nimport {\n    getDefaultValue,\n    getValueEditorInfo,\n} from \"@web/core/tree_editor/tree_editor_value_editors\";\nimport { ModelFieldSelector } from \"@web/core/model_field_selector/model_field_selector\";\nimport { useLoadFieldInfo } from \"@web/core/model_field_selector/utils\";\nimport { deepEqual, shallowEqual } from \"@web/core/utils/objects\";\n\nconst TRUE_TREE = condition(1, \"=\", 1);\n\nfunction collectDifferences(tree, otherTree) {\n    // some differences shadow the other differences \"below\":\n    if (tree.type !== otherTree.type) {\n        return [{ type: \"other\" }];\n    }\n    if (tree.negate !== otherTree.negate) {\n        return [{ type: \"other\" }];\n    }\n    if (tree.type === \"condition\") {\n        if (formatValue(tree.path) !== formatValue(otherTree.path)) {\n            return [{ type: \"other\" }];\n        }\n        if (formatValue(tree.value) !== formatValue(otherTree.value)) {\n            return [{ type: \"other\" }];\n        }\n        if (formatValue(tree.operator) !== formatValue(otherTree.operator)) {\n            if (tree.operator === \"!=\" && otherTree.operator === \"set\") {\n                return [{ type: \"replacement\", tree, operator: \"set\" }];\n            } else if (tree.operator === \"=\" && otherTree.operator === \"not_set\") {\n                return [{ type: \"replacement\", tree, operator: \"not_set\" }];\n            } else {\n                return [{ type: \"other\" }];\n            }\n        }\n        return [];\n    }\n    if (tree.value !== otherTree.value) {\n        return [{ type: \"other\" }];\n    }\n    if (tree.type === \"complex_condition\") {\n        return [];\n    }\n    if (tree.children.length !== otherTree.children.length) {\n        return [{ type: \"other\" }];\n    }\n    const diffs = [];\n    for (let i = 0; i < tree.children.length; i++) {\n        const child = tree.children[i];\n        const otherChild = otherTree.children[i];\n        const childDiffs = collectDifferences(child, otherChild);\n        if (childDiffs.some((d) => d.type !== \"replacement\")) {\n            return [{ type: \"other\" }];\n        }\n        diffs.push(...childDiffs);\n    }\n    return diffs;\n}\n\nfunction restoreVirtualOperators(tree, otherTree) {\n    const diffs = collectDifferences(tree, otherTree);\n    // note that the array diffs is homogeneous:\n    // we have diffs of the form [], [other], [repl, ..., repl]\n    if (diffs.some((d) => d.type !== \"replacement\")) {\n        return;\n    }\n    for (const { tree, operator } of diffs) {\n        tree.operator = operator;\n    }\n}\n\nexport class TreeEditor extends Component {\n    static template = \"web.TreeEditor\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n        ModelFieldSelector,\n        TreeEditor,\n    };\n    static props = {\n        tree: Object,\n        resModel: String,\n        update: Function,\n        getDefaultCondition: Function,\n        getPathEditorInfo: Function,\n        getOperatorEditorInfo: Function,\n        getDefaultOperator: Function,\n        readonly: { type: Boolean, optional: true },\n        slots: { type: Object, optional: true },\n        isDebugMode: { type: Boolean, optional: true },\n        defaultConnector: { type: [{ value: \"&\" }, { value: \"|\" }], optional: true },\n        isSubTree: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        defaultConnector: \"&\",\n        readonly: false,\n        isSubTree: false,\n    };\n\n    setup() {\n        this.isTree = isTree;\n        this.fieldService = useService(\"field\");\n        this.nameService = useService(\"name\");\n        this.loadFieldInfo = useLoadFieldInfo(this.fieldService);\n        this.makeGetFieldDef = useMakeGetFieldDef(this.fieldService);\n        this.makeGetConditionDescription = useMakeGetConditionDescription(\n            this.fieldService,\n            this.nameService\n        );\n        onWillStart(() => this.onPropsUpdated(this.props));\n        onWillUpdateProps((nextProps) => this.onPropsUpdated(nextProps));\n    }\n\n    async onPropsUpdated(props) {\n        this.tree = cloneTree(props.tree);\n        if (shallowEqual(this.tree, TRUE_TREE)) {\n            this.tree = connector(props.defaultConnector);\n        } else if (this.tree.type !== \"connector\") {\n            this.tree = connector(props.defaultConnector, [this.tree]);\n        }\n\n        if (this.previousTree) {\n            // find \"first\" difference\n            restoreVirtualOperators(this.tree, this.previousTree);\n            this.previousTree = null;\n        }\n\n        const [fieldDefs, getFieldDef] = await Promise.all([\n            this.fieldService.loadFields(props.resModel),\n            this.makeGetFieldDef(props.resModel, this.tree),\n        ]);\n        this.getFieldDef = getFieldDef;\n        this.defaultCondition = props.getDefaultCondition(fieldDefs);\n\n        if (props.readonly) {\n            this.getConditionDescription = await this.makeGetConditionDescription(\n                props.resModel,\n                this.tree,\n                this.getFieldDef\n            );\n        }\n    }\n\n    get className() {\n        return `${this.props.readonly ? \"o_read_mode\" : \"o_edit_mode\"}`;\n    }\n\n    get isDebugMode() {\n        return this.props.isDebugMode !== undefined ? this.props.isDebugMode : !!this.env.debug;\n    }\n\n    notifyChanges() {\n        this.previousTree = cloneTree(this.tree);\n        this.props.update(this.tree);\n    }\n\n    updateConnector(node, value) {\n        node.value = value;\n        node.negate = false;\n        this.notifyChanges();\n    }\n\n    updateComplexCondition(node, value) {\n        node.value = value;\n        this.notifyChanges();\n    }\n\n    createNewLeaf() {\n        return cloneTree(this.defaultCondition);\n    }\n\n    createNewBranch(value) {\n        return connector(value, [this.createNewLeaf(), this.createNewLeaf()]);\n    }\n\n    insertRootLeaf(parent) {\n        parent.children.push(this.createNewLeaf());\n        this.notifyChanges();\n    }\n\n    insertLeaf(parent, node) {\n        const newNode = node.type !== \"connector\" ? cloneTree(node) : this.createNewLeaf();\n        const index = parent.children.indexOf(node);\n        parent.children.splice(index + 1, 0, newNode);\n        this.notifyChanges();\n    }\n\n    insertBranch(parent, node) {\n        const nextConnector = parent.value === \"&\" ? \"|\" : \"&\";\n        const newNode = this.createNewBranch(nextConnector);\n        const index = parent.children.indexOf(node);\n        parent.children.splice(index + 1, 0, newNode);\n        this.notifyChanges();\n    }\n\n    delete(parent, node) {\n        const index = parent.children.indexOf(node);\n        parent.children.splice(index, 1);\n        this.notifyChanges();\n    }\n\n    getResModel(node) {\n        const fieldDef = this.getFieldDef(node.path);\n        const resModel = getResModel(fieldDef);\n        return resModel;\n    }\n\n    getPathEditorInfo() {\n        return this.props.getPathEditorInfo(this.props.resModel, this.defaultCondition);\n    }\n\n    getOperatorEditorInfo(node) {\n        const fieldDef = this.getFieldDef(node.path);\n        return this.props.getOperatorEditorInfo(fieldDef);\n    }\n\n    getValueEditorInfo(node) {\n        const fieldDef = this.getFieldDef(node.path);\n        return getValueEditorInfo(fieldDef, node.operator);\n    }\n\n    async updatePath(node, path) {\n        const { fieldDef } = await this.loadFieldInfo(this.props.resModel, path);\n        node.path = path;\n        node.negate = false;\n        node.operator = this.props.getDefaultOperator(fieldDef);\n        node.value = getDefaultValue(fieldDef, node.operator);\n        this.notifyChanges();\n    }\n\n    updateLeafOperator(node, operator, negate) {\n        const previousNode = cloneTree(node);\n        const fieldDef = this.getFieldDef(node.path);\n        node.negate = negate;\n        node.operator = operator;\n        node.value = getDefaultValue(fieldDef, operator, node.value);\n        if (deepEqual(removeVirtualOperators(node), removeVirtualOperators(previousNode))) {\n            // no interesting changes for parent\n            // this means that parent might not render the domain selector\n            // but we need to udpate editors\n            this.render();\n        }\n        this.notifyChanges();\n    }\n\n    updateLeafValue(node, value) {\n        node.value = value;\n        this.notifyChanges();\n    }\n\n    highlightNode(target) {\n        const nodeEl = target.closest(\".o_tree_editor_node\");\n        nodeEl.classList.toggle(\"o_hovered_button\");\n    }\n}\n", "import { MultiRecordSelector } from \"@web/core/record_selectors/multi_record_selector\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatAST, toPyValue } from \"@web/core/py_js/py_utils\";\nimport { Expression } from \"@web/core/tree_editor/condition_tree\";\nimport { RecordSelector } from \"@web/core/record_selectors/record_selector\";\n\nexport const isId = (val) => Number.isInteger(val) && val >= 1;\n\nexport const getFormat = (val, displayNames) => {\n    let text;\n    let colorIndex;\n    if (isId(val)) {\n        text =\n            typeof displayNames[val] === \"string\"\n                ? displayNames[val]\n                : _t(\"Inaccessible/missing record ID: %s\", val);\n        colorIndex = typeof displayNames[val] === \"string\" ? 0 : 2; // 0 = grey, 2 = orange\n    } else {\n        text =\n            val instanceof Expression\n                ? String(val)\n                : _t(\"Invalid record ID: %s\", formatAST(toPyValue(val)));\n        colorIndex = val instanceof Expression ? 2 : 1; // 1 = red\n    }\n    return { text, colorIndex };\n};\n\nexport class DomainSelectorAutocomplete extends MultiRecordSelector {\n    static props = {\n        ...MultiRecordSelector.props,\n        resIds: true, //resIds could be an array of ids or an array of expressions\n    };\n\n    getIds(props = this.props) {\n        return props.resIds.filter((val) => isId(val));\n    }\n\n    getTags(props, displayNames) {\n        return props.resIds.map((val, index) => {\n            const { text, colorIndex } = getFormat(val, displayNames);\n            return {\n                text,\n                colorIndex,\n                onDelete: () => {\n                    this.props.update([\n                        ...this.props.resIds.slice(0, index),\n                        ...this.props.resIds.slice(index + 1),\n                    ]);\n                },\n            };\n        });\n    }\n}\n\nexport class DomainSelectorSingleAutocomplete extends RecordSelector {\n    static props = {\n        ...RecordSelector.props,\n        resId: true,\n    };\n\n    getDisplayName(props = this.props, displayNames) {\n        const { resId } = props;\n        if (resId === false) {\n            return \"\";\n        }\n        const { text } = getFormat(resId, displayNames);\n        return text;\n    }\n\n    getIds(props = this.props) {\n        if (isId(props.resId)) {\n            return [props.resId];\n        }\n        return [];\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class Input extends Component {\n    static props = [\"value\", \"update\", \"startEmpty?\"];\n    static template = \"web.TreeEditor.Input\";\n}\n\nexport class Select extends Component {\n    static props = [\"value\", \"update\", \"options\", \"addBlankOption?\"];\n    static template = \"web.TreeEditor.Select\";\n\n    deserialize(value) {\n        return JSON.parse(value);\n    }\n\n    serialize(value) {\n        return JSON.stringify(value);\n    }\n}\n\nexport class Range extends Component {\n    static props = [\"value\", \"update\", \"editorInfo\"];\n    static template = \"web.TreeEditor.Range\";\n\n    update(index, newValue) {\n        const result = [...this.props.value];\n        result[index] = newValue;\n        return this.props.update(result);\n    }\n}\n\nexport class Within extends Component {\n    static props = [\"value\", \"update\", \"amountEditorInfo\", \"optionEditorInfo\"];\n    static template = \"web.TreeEditor.Within\";\n    static components = { Input, Select };\n    static options = [\n        [\"days\", _t(\"days\")],\n        [\"weeks\", _t(\"weeks\")],\n        [\"months\", _t(\"months\")],\n        [\"years\", _t(\"years\")],\n    ];\n    update(index, newValue) {\n        const result = [...this.props.value];\n        result[index] = newValue;\n        return this.props.update(result);\n    }\n}\n\nexport class List extends Component {\n    static components = { TagsList };\n    static props = [\"value\", \"update\", \"editorInfo\"];\n    static template = \"web.TreeEditor.List\";\n\n    get tags() {\n        const { isSupported, stringify } = this.props.editorInfo;\n        return this.props.value.map((val, index) => ({\n            text: stringify(val),\n            colorIndex: isSupported(val) ? 0 : 2,\n            onDelete: () => {\n                this.props.update([\n                    ...this.props.value.slice(0, index),\n                    ...this.props.value.slice(index + 1),\n                ]);\n            },\n        }));\n    }\n\n    update(newValue) {\n        return this.props.update([...this.props.value, newValue]);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport {\n    formatValue,\n    TERM_OPERATORS_NEGATION,\n    toValue,\n} from \"@web/core/tree_editor/condition_tree\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { parseExpr } from \"@web/core/py_js/py\";\nimport { Select } from \"@web/core/tree_editor/tree_editor_components\";\n\nconst OPERATOR_DESCRIPTIONS = {\n    // valid operators (see TERM_OPERATORS in expression.py)\n    \"=\": \"=\",\n    \"!=\": \"!=\",\n    \"<=\": \"<=\",\n    \"<\": \"<\",\n    \">\": \">\",\n    \">=\": \">=\",\n    \"=?\": \"=?\",\n    \"=like\": _t(\"=like\"),\n    \"=ilike\": _t(\"=ilike\"),\n    like: _t(\"like\"),\n    \"not like\": _t(\"not like\"),\n    ilike: _t(\"contains\"),\n    \"not ilike\": _t(\"does not contain\"),\n    in: _t(\"is in\"),\n    \"not in\": _t(\"is not in\"),\n    child_of: _t(\"child of\"),\n    parent_of: _t(\"parent of\"),\n\n    // virtual operators (replace = and != in some cases)\n    is: _t(\"is\"),\n    is_not: _t(\"is not\"),\n    set: _t(\"is set\"),\n    not_set: _t(\"is not set\"),\n\n    starts_with: _t(\"starts with\"),\n    ends_with: _t(\"ends with\"),\n\n    // virtual operator (equivalent to a couple (>=,<=))\n    between: _t(\"is between\"),\n    within: _t(\"is within\"),\n\n    any: (fieldDefType) => {\n        switch (fieldDefType) {\n            case \"many2one\":\n                return _t(\"matches\");\n            default:\n                return _t(\"match\");\n        }\n    },\n    \"not any\": (fieldDefType) => {\n        switch (fieldDefType) {\n            case \"many2one\":\n                return _t(\"matches none of\");\n            default:\n                return _t(\"match none of\");\n        }\n    },\n};\n\nfunction toKey(operator, negate = false) {\n    if (!negate && typeof operator === \"string\" && operator in OPERATOR_DESCRIPTIONS) {\n        // this case is the main one. We keep it simple\n        return operator;\n    }\n    return JSON.stringify([formatValue(operator), negate]);\n}\n\nfunction toOperator(key) {\n    if (!key.includes(\"[\")) {\n        return [key, false];\n    }\n    const [expr, negate] = JSON.parse(key);\n    return [toValue(parseExpr(expr)), negate];\n}\n\nfunction getOperatorDescription(operator, fieldDefType) {\n    const description = OPERATOR_DESCRIPTIONS[operator];\n    if (\n        typeof description === \"function\" &&\n        description.constructor?.name !== \"LazyTranslatedString\"\n    ) {\n        return description(fieldDefType);\n    }\n    return description;\n}\n\nexport function getOperatorLabel(operator, fieldDefType, negate = false) {\n    let label;\n    if (typeof operator === \"string\" && operator in OPERATOR_DESCRIPTIONS) {\n        if (negate && operator in TERM_OPERATORS_NEGATION) {\n            return getOperatorDescription(TERM_OPERATORS_NEGATION[operator], fieldDefType);\n        }\n        label = getOperatorDescription(operator, fieldDefType);\n    } else {\n        label = formatValue(operator);\n    }\n    if (negate) {\n        return sprintf(`not %s`, label);\n    }\n    return label;\n}\n\nfunction getOperatorInfo(operator, fieldDefType, negate = false) {\n    const key = toKey(operator, negate);\n    const label = getOperatorLabel(operator, fieldDefType, negate);\n    return [key, label];\n}\n\nexport function getOperatorEditorInfo(operators, fieldDef) {\n    const defaultOperator = operators[0];\n    const operatorsInfo = operators.map((operator) => getOperatorInfo(operator, fieldDef?.type));\n    return {\n        component: Select,\n        extractProps: ({ update, value: [operator, negate] }) => {\n            const [operatorKey, operatorLabel] = getOperatorInfo(operator, fieldDef?.type, negate);\n            const options = [...operatorsInfo];\n            if (!options.some(([key]) => key === operatorKey)) {\n                options.push([operatorKey, operatorLabel]);\n            }\n            return {\n                value: operatorKey,\n                update: (operatorKey) => update(...toOperator(operatorKey)),\n                options,\n            };\n        },\n        defaultValue: () => defaultOperator,\n        isSupported: ([operator]) =>\n            typeof operator === \"string\" && operator in OPERATOR_DESCRIPTIONS, // should depend on fieldDef too... (e.g. parent_id does not always make sense)\n        message: _t(\"Operator not supported\"),\n        stringify: ([operator, negate]) => getOperatorLabel(operator, negate),\n    };\n}\n", "import {\n    deserializeDate,\n    deserializeDateTime,\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { DateTimeInput } from \"@web/core/datetime/datetime_input\";\nimport {\n    DomainSelectorAutocomplete,\n    DomainSelectorSingleAutocomplete,\n} from \"@web/core/tree_editor/tree_editor_autocomplete\";\nimport { unique } from \"@web/core/utils/arrays\";\nimport { Input, Select, List, Range, Within } from \"@web/core/tree_editor/tree_editor_components\";\nimport { connector, formatValue, isTree } from \"@web/core/tree_editor/condition_tree\";\nimport { getResModel, disambiguate, isId } from \"@web/core/tree_editor/utils\";\n\nconst { DateTime } = luxon;\n\n// ============================================================================\n\nconst formatters = registry.category(\"formatters\");\nconst parsers = registry.category(\"parsers\");\n\nfunction parseValue(fieldType, value) {\n    const parser = parsers.get(fieldType, (value) => value);\n    try {\n        return parser(value);\n    } catch {\n        return value;\n    }\n}\n\nfunction isParsable(fieldType, value) {\n    const parser = parsers.get(fieldType, (value) => value);\n    try {\n        parser(value);\n    } catch {\n        return false;\n    }\n    return true;\n}\n\nfunction genericSerializeDate(type, value) {\n    return type === \"date\" ? serializeDate(value) : serializeDateTime(value);\n}\n\nfunction genericDeserializeDate(type, value) {\n    return type === \"date\" ? deserializeDate(value) : deserializeDateTime(value);\n}\n\nconst STRING_EDITOR = {\n    component: Input,\n    extractProps: ({ value, update }) => ({ value, update }),\n    isSupported: (value) => typeof value === \"string\",\n    defaultValue: () => \"\",\n};\n\nfunction makeSelectEditor(options, params = {}) {\n    const getOption = (value) => options.find(([v]) => v === value) || null;\n    return {\n        component: Select,\n        extractProps: ({ value, update }) => ({\n            value,\n            update,\n            options,\n            addBlankOption: params.addBlankOption,\n        }),\n        isSupported: (value) => Boolean(getOption(value)),\n        defaultValue: () => options[0]?.[0] ?? false,\n        stringify: (value, disambiguate) => {\n            const option = getOption(value);\n            return option ? option[1] : disambiguate ? formatValue(value) : String(value);\n        },\n        message: _t(\"Value not in selection\"),\n    };\n}\n\nfunction makeAutoCompleteEditor(fieldDef) {\n    return {\n        component: DomainSelectorAutocomplete,\n        extractProps: ({ value, update }) => {\n            return {\n                resModel: getResModel(fieldDef),\n                fieldString: fieldDef.string,\n                update: (value) => update(unique(value)),\n                resIds: unique(value),\n            };\n        },\n        isSupported: (value) => Array.isArray(value),\n        defaultValue: () => [],\n    };\n}\n\n// ============================================================================\n\nfunction getPartialValueEditorInfo(fieldDef, operator, params = {}) {\n    switch (operator) {\n        case \"set\":\n        case \"not_set\":\n            return {\n                component: null,\n                extractProps: null,\n                isSupported: (value) => value === false,\n                defaultValue: () => false,\n            };\n        case \"=like\":\n        case \"=ilike\":\n        case \"like\":\n        case \"not like\":\n        case \"ilike\":\n        case \"not ilike\":\n            return STRING_EDITOR;\n        case \"between\": {\n            const editorInfo = getValueEditorInfo(fieldDef, \"=\");\n            return {\n                component: Range,\n                extractProps: ({ value, update }) => ({\n                    value,\n                    update,\n                    editorInfo,\n                }),\n                isSupported: (value) => Array.isArray(value) && value.length === 2,\n                defaultValue: () => {\n                    const { defaultValue } = editorInfo;\n                    return [defaultValue(), defaultValue()];\n                },\n            };\n        }\n        case \"within\": {\n            return {\n                component: Within,\n                extractProps: ({ value, update }) => ({\n                    value,\n                    update,\n                    amountEditorInfo: getValueEditorInfo({ type: \"integer\" }, \"=\"),\n                    optionEditorInfo: makeSelectEditor(Within.options),\n                }),\n                isSupported: (value) =>\n                    Array.isArray(value) &&\n                    value.length === 3 &&\n                    typeof value[1] === \"string\" &&\n                    value[2] === fieldDef.type,\n                defaultValue: () => {\n                    return [-1, \"months\", fieldDef.type];\n                },\n            };\n        }\n        case \"in\":\n        case \"not in\": {\n            switch (fieldDef.type) {\n                case \"tags\":\n                    return STRING_EDITOR;\n                case \"many2one\":\n                case \"many2many\":\n                case \"one2many\":\n                    return makeAutoCompleteEditor(fieldDef);\n                default: {\n                    const editorInfo = getValueEditorInfo(fieldDef, \"=\", {\n                        addBlankOption: true,\n                        startEmpty: true,\n                    });\n                    return {\n                        component: List,\n                        extractProps: ({ value, update }) => {\n                            if (!disambiguate(value)) {\n                                const { stringify } = editorInfo;\n                                editorInfo.stringify = (val) => stringify(val, false);\n                            }\n                            return {\n                                value,\n                                update,\n                                editorInfo,\n                            };\n                        },\n                        isSupported: (value) => Array.isArray(value),\n                        defaultValue: () => [],\n                    };\n                }\n            }\n        }\n        case \"any\":\n        case \"not any\": {\n            switch (fieldDef.type) {\n                case \"many2one\":\n                case \"many2many\":\n                case \"one2many\": {\n                    return {\n                        component: null,\n                        extractProps: null,\n                        isSupported: isTree,\n                        defaultValue: () => connector(\"&\"),\n                    };\n                }\n            }\n        }\n    }\n\n    const { type } = fieldDef;\n    switch (type) {\n        case \"integer\":\n        case \"float\":\n        case \"monetary\": {\n            const formatType = type === \"integer\" ? \"integer\" : \"float\";\n            return {\n                component: Input,\n                extractProps: ({ value, update }) => ({\n                    value: String(value),\n                    update: (value) => update(parseValue(formatType, value)),\n                    startEmpty: params.startEmpty,\n                }),\n                isSupported: () => true,\n                defaultValue: () => 1,\n                shouldResetValue: (value) => parseValue(formatType, value) === value,\n            };\n        }\n        case \"date\":\n        case \"datetime\":\n            return {\n                component: DateTimeInput,\n                extractProps: ({ value, update }) => ({\n                    value:\n                        params.startEmpty || value === false\n                            ? false\n                            : genericDeserializeDate(type, value),\n                    type,\n                    onApply: (value) => {\n                        if (!params.startEmpty || value) {\n                            update(genericSerializeDate(type, value || DateTime.local()));\n                        }\n                    },\n                }),\n                isSupported: (value) =>\n                    value === false || (typeof value === \"string\" && isParsable(type, value)),\n                defaultValue: () => genericSerializeDate(type, DateTime.local()),\n                stringify: (value) => {\n                    if (value === false) {\n                        return _t(\"False\");\n                    }\n                    if (typeof value === \"string\" && isParsable(type, value)) {\n                        const formatter = formatters.get(type, formatValue);\n                        return formatter(genericDeserializeDate(type, value));\n                    }\n                    return formatValue(value);\n                },\n                message: _t(\"Not a valid %s\", type),\n            };\n        case \"char\":\n        case \"html\":\n        case \"text\":\n            return STRING_EDITOR;\n        case \"boolean\": {\n            if ([\"is\", \"is_not\"].includes(operator)) {\n                const options = [\n                    [true, _t(\"set\")],\n                    [false, _t(\"not set\")],\n                ];\n                return makeSelectEditor(options, params);\n            }\n            const options = [\n                [true, _t(\"True\")],\n                [false, _t(\"False\")],\n            ];\n            return makeSelectEditor(options, params);\n        }\n        case \"many2one\": {\n            if ([\"=\", \"!=\", \"parent_of\", \"child_of\"].includes(operator)) {\n                return {\n                    component: DomainSelectorSingleAutocomplete,\n                    extractProps: ({ value, update }) => {\n                        return {\n                            resModel: getResModel(fieldDef),\n                            fieldString: fieldDef.string,\n                            update,\n                            resId: value,\n                        };\n                    },\n                    isSupported: () => true,\n                    defaultValue: () => false,\n                    shouldResetValue: (value) => value !== false && !isId(value),\n                };\n            }\n            break;\n        }\n        case \"many2many\":\n        case \"one2many\":\n            if ([\"=\", \"!=\"].includes(operator)) {\n                return makeAutoCompleteEditor(fieldDef);\n            }\n            break;\n        case \"selection\": {\n            const options = fieldDef.selection || [];\n            return makeSelectEditor(options, params);\n        }\n        case undefined: {\n            const options = [[1, \"1\"]];\n            return makeSelectEditor(options, params);\n        }\n    }\n\n    // Global default for visualization mainly. It is there to visualize what\n    // has been produced in the debug textarea (in o_domain_selector_debug_container)\n    // It is hardly useful to produce a string in general.\n    return {\n        component: Input,\n        extractProps: ({ value, update }) => ({\n            value: String(value),\n            update,\n        }),\n        isSupported: () => true,\n        defaultValue: () => \"\",\n    };\n}\n\nexport function getValueEditorInfo(fieldDef, operator, options = {}) {\n    const info = getPartialValueEditorInfo(fieldDef || {}, operator, options);\n    return {\n        extractProps: ({ value, update }) => ({ value, update }),\n        message: _t(\"Value not supported\"),\n        stringify: (val, disambiguate = true) => {\n            if (disambiguate) {\n                return formatValue(val);\n            }\n            return String(val);\n        },\n        ...info,\n    };\n}\n\nexport function getDefaultValue(fieldDef, operator, value = null) {\n    const { isSupported, shouldResetValue, defaultValue } = getValueEditorInfo(fieldDef, operator);\n    if (value === null || !isSupported(value) || shouldResetValue?.(value)) {\n        return defaultValue();\n    }\n    return value;\n}\n", "import { unique, zip } from \"@web/core/utils/arrays\";\nimport { getOperatorLabel } from \"@web/core/tree_editor/tree_editor_operator_editor\";\nimport {\n    Expression,\n    condition,\n    createVirtualOperators,\n    normalizeValue,\n    isTree,\n} from \"@web/core/tree_editor/condition_tree\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    deserializeDate,\n    deserializeDateTime,\n    formatDate,\n    formatDateTime,\n} from \"@web/core/l10n/dates\";\nimport { useLoadFieldInfo, useLoadPathDescription } from \"@web/core/model_field_selector/utils\";\nimport { Within } from \"./tree_editor_components\";\n\n/**\n * @param {import(\"@web/core/tree_editor/condition_tree\").Value} val\n * @param {boolean} disambiguate\n * @param {Object|null} fieldDef\n * @param {Object} displayNames\n * @returns\n */\nfunction formatValue(val, disambiguate, fieldDef, displayNames) {\n    if (val instanceof Expression) {\n        return val.toString();\n    }\n    if (displayNames && isId(val)) {\n        if (typeof displayNames[val] === \"string\") {\n            val = displayNames[val];\n        } else {\n            return _t(\"Inaccessible/missing record ID: %s\", val);\n        }\n    }\n    if (fieldDef?.type === \"selection\") {\n        const [, label] = (fieldDef.selection || []).find(([v]) => v === val) || [];\n        if (label !== undefined) {\n            val = label;\n        }\n    }\n    if (typeof val === \"string\") {\n        if (fieldDef?.type === \"datetime\") {\n            return formatDateTime(deserializeDateTime(val));\n        }\n        if (fieldDef?.type === \"date\") {\n            return formatDate(deserializeDate(val));\n        }\n    }\n    if (disambiguate && typeof val === \"string\") {\n        return JSON.stringify(val);\n    }\n    return val;\n}\n\nexport function isId(value) {\n    return Number.isInteger(value) && value >= 1;\n}\n\nexport function disambiguate(value, displayNames) {\n    if (!Array.isArray(value)) {\n        return value === \"\";\n    }\n    let hasSomeString = false;\n    let hasSomethingElse = false;\n    for (const val of value) {\n        if (val === \"\") {\n            return true;\n        }\n        if (typeof val === \"string\" || (displayNames && isId(val))) {\n            hasSomeString = true;\n        } else {\n            hasSomethingElse = true;\n        }\n    }\n    return hasSomeString && hasSomethingElse;\n}\n\nexport function useMakeGetFieldDef(fieldService) {\n    fieldService ||= useService(\"field\");\n    const loadFieldInfo = useLoadFieldInfo(fieldService);\n    return async (resModel, tree, additionalsPath = []) => {\n        const pathsInTree = getPathsInTree(tree);\n        const paths = new Set([...pathsInTree, ...additionalsPath]);\n        const promises = [];\n        const fieldDefs = {};\n        for (const path of paths) {\n            if (typeof path === \"string\") {\n                promises.push(\n                    loadFieldInfo(resModel, path).then(({ fieldDef }) => {\n                        fieldDefs[path] = fieldDef;\n                    })\n                );\n            }\n        }\n        await Promise.all(promises);\n        return (path) => {\n            if (typeof path === \"string\") {\n                return fieldDefs[path];\n            }\n            return null;\n        };\n    };\n}\n\nfunction useGetTreePathDescription(fieldService) {\n    fieldService ||= useService(\"field\");\n    const loadPathDescription = useLoadPathDescription(fieldService);\n    return async (resModel, tree) => {\n        const paths = getPathsInTree(tree);\n        const promises = [];\n        const pathDescriptions = new Map();\n        for (const path of paths) {\n            promises.push(\n                loadPathDescription(resModel, path).then(({ displayNames }) => {\n                    pathDescriptions.set(path, displayNames.join(\" \\u2794 \"));\n                })\n            );\n        }\n        await Promise.all(promises);\n        return (path) => pathDescriptions.get(path);\n    };\n}\n\nasync function getDisplayNames(tree, getFieldDef, nameService) {\n    const resIdsByModel = extractIdsFromTree(tree, getFieldDef);\n    const proms = [];\n    const resModels = [];\n    for (const [resModel, resIds] of Object.entries(resIdsByModel)) {\n        resModels.push(resModel);\n        proms.push(nameService.loadDisplayNames(resModel, resIds));\n    }\n    return Object.fromEntries(zip(resModels, await Promise.all(proms)));\n}\n\nexport function useMakeGetConditionDescription(fieldService, nameService) {\n    const makeGetPathDescriptions = useGetTreePathDescription(fieldService);\n    return async (resModel, tree, getFieldDef) => {\n        tree = simplifyTree(tree);\n        const [displayNames, getPathDescription] = await Promise.all([\n            getDisplayNames(tree, getFieldDef, nameService),\n            makeGetPathDescriptions(resModel, tree),\n        ]);\n        return (node) =>\n            _getConditionDescription(node, getFieldDef, getPathDescription, displayNames);\n    };\n}\n\nfunction _getConditionDescription(node, getFieldDef, getPathDescription, displayNames) {\n    const nodeWithVirtualOperators = createVirtualOperators(node, { getFieldDef });\n    const { operator, negate, value, path } = nodeWithVirtualOperators;\n    const fieldDef = getFieldDef(path);\n    const operatorLabel = getOperatorLabel(operator, fieldDef?.type, negate);\n    const pathDescription = getPathDescription(path);\n    const description = {\n        pathDescription,\n        operatorDescription: operatorLabel,\n        valueDescription: null,\n    };\n\n    if (isTree(node.value)) {\n        return description;\n    }\n    if ([\"set\", \"not_set\"].includes(operator)) {\n        return description;\n    }\n    if ([\"is\", \"is_not\"].includes(operator)) {\n        description.valueDescription = {\n            values: [value ? _t(\"set\") : _t(\"not set\")],\n            join: \"\",\n            addParenthesis: false,\n        };\n        return description;\n    }\n\n    const coModeldisplayNames = displayNames[getResModel(fieldDef)];\n    const dis = disambiguate(value, coModeldisplayNames);\n    const values =\n        operator == \"within\"\n            ? [value[0], Within.options.find((option) => option[0] === value[1])[1]]\n            : (Array.isArray(value) ? value : [value]).map((val) =>\n                  formatValue(val, dis, fieldDef, coModeldisplayNames)\n              );\n    let join;\n    let addParenthesis = Array.isArray(value);\n    switch (operator) {\n        case \"between\":\n            join = _t(\"and\");\n            addParenthesis = false;\n            break;\n        case \"within\":\n            join = \" \";\n            addParenthesis = false;\n            break;\n        case \"in\":\n        case \"not in\":\n            join = \",\";\n            break;\n        default:\n            join = _t(\"or\");\n    }\n    description.valueDescription = { values, join, addParenthesis };\n    return description;\n}\n\nexport function useGetTreeDescription(fieldService, nameService) {\n    fieldService ||= useService(\"field\");\n    nameService ||= useService(\"name\");\n    const makeGetFieldDef = useMakeGetFieldDef(fieldService);\n    const makeGetConditionDescription = useMakeGetConditionDescription(fieldService, nameService);\n    return async (resModel, tree) => {\n        async function getTreeDescription(resModel, tree, isSubExpression = false) {\n            tree = simplifyTree(tree);\n            if (tree.type === \"connector\") {\n                // we assume that the domain tree is normalized (--> there is at least two children)\n                const childDescriptions = tree.children.map((node) =>\n                    getTreeDescription(resModel, node, true)\n                );\n                const separator = tree.value === \"&\" ? _t(\"and\") : _t(\"or\");\n                let description = await Promise.all(childDescriptions);\n                description = description.join(` ${separator} `);\n                if (isSubExpression || tree.negate) {\n                    description = `( ${description} )`;\n                }\n                if (tree.negate) {\n                    description = `! ${description}`;\n                }\n                return description;\n            }\n            const getFieldDef = await makeGetFieldDef(resModel, tree);\n            const getConditionDescription = await makeGetConditionDescription(\n                resModel,\n                tree,\n                getFieldDef\n            );\n            const { pathDescription, operatorDescription, valueDescription } =\n                getConditionDescription(tree);\n            const stringDescription = [pathDescription, operatorDescription];\n            if (valueDescription) {\n                const { values, join, addParenthesis } = valueDescription;\n                const jointedValues = values.join(` ${join} `);\n                stringDescription.push(addParenthesis ? `( ${jointedValues} )` : jointedValues);\n            } else if (isTree(tree.value)) {\n                const _fieldDef = getFieldDef(tree.path);\n                const _resModel = getResModel(_fieldDef);\n                const _tree = tree.value;\n                const description = await getTreeDescription(_resModel, _tree);\n                stringDescription.push(`( ${description} )`);\n            }\n            return stringDescription.join(\" \");\n        }\n        return getTreeDescription(resModel, tree);\n    };\n}\n\nexport function getResModel(fieldDef) {\n    if (fieldDef) {\n        return fieldDef.is_property ? fieldDef.comodel : fieldDef.relation;\n    }\n    return null;\n}\n\nfunction extractIdsFromTree(tree, getFieldDef) {\n    const idsByModel = _extractIdsRecursive(tree, getFieldDef, {});\n\n    for (const resModel in idsByModel) {\n        idsByModel[resModel] = unique(idsByModel[resModel]);\n    }\n\n    return idsByModel;\n}\n\nfunction _extractIdsRecursive(tree, getFieldDef, idsByModel) {\n    if (tree.type === \"condition\") {\n        const fieldDef = getFieldDef(tree.path);\n        if ([\"many2one\", \"many2many\", \"one2many\"].includes(fieldDef?.type)) {\n            const value = tree.value;\n            const values = Array.isArray(value) ? value : [value];\n            const ids = values.filter((val) => isId(val));\n            const resModel = getResModel(fieldDef);\n            if (ids.length) {\n                if (!idsByModel[resModel]) {\n                    idsByModel[resModel] = [];\n                }\n                idsByModel[resModel].push(...ids);\n            }\n        }\n    }\n    if (tree.type === \"connector\") {\n        for (const child of tree.children) {\n            _extractIdsRecursive(child, getFieldDef, idsByModel);\n        }\n    }\n    return idsByModel;\n}\n\nexport function getPathsInTree(tree) {\n    const paths = [];\n    if (tree.type === \"condition\") {\n        paths.push(tree.path);\n    }\n    if (tree.type === \"connector\" && tree.children) {\n        for (const child of tree.children) {\n            paths.push(...getPathsInTree(child));\n        }\n    }\n    return unique(paths);\n}\n\nconst SPECIAL_FIELDS = [\"country_id\", \"user_id\", \"partner_id\", \"stage_id\", \"id\"];\n\nexport function getDefaultPath(fieldDefs) {\n    for (const name of SPECIAL_FIELDS) {\n        const fieldDef = fieldDefs[name];\n        if (fieldDef) {\n            return fieldDef.name;\n        }\n    }\n    const name = Object.keys(fieldDefs)[0];\n    if (name) {\n        return name;\n    }\n    throw new Error(`No field found`);\n}\n\n/**\n * @param {Tree} tree\n * @returns {tree}\n */\nfunction simplifyTree(tree) {\n    if (tree.type === \"condition\") {\n        return tree;\n    }\n    const processedChildren = tree.children.map(simplifyTree);\n    if (tree.value === \"&\") {\n        return { ...tree, children: processedChildren };\n    }\n    const children = [];\n    const childrenByPath = {};\n    for (const child of processedChildren) {\n        if (\n            child.type === \"connector\" ||\n            typeof child.path !== \"string\" ||\n            ![\"=\", \"in\"].includes(child.operator)\n        ) {\n            children.push(child);\n        } else {\n            if (!childrenByPath[child.path]) {\n                childrenByPath[child.path] = [];\n            }\n            childrenByPath[child.path].push(child);\n        }\n    }\n    for (const path in childrenByPath) {\n        if (childrenByPath[path].length === 1) {\n            children.push(childrenByPath[path][0]);\n            continue;\n        }\n        const value = [];\n        for (const child of childrenByPath[path]) {\n            if (child.operator === \"=\") {\n                value.push(child.value);\n            } else {\n                value.push(...child.value);\n            }\n        }\n        children.push(condition(path, \"in\", normalizeValue(value)));\n    }\n    if (children.length === 1) {\n        return { ...children[0] };\n    }\n    return { ...tree, children };\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\n\nimport { EventBus, Component, useState, xml } from \"@odoo/owl\";\n\nexport class BlockUI extends Component {\n    static props = {\n        bus: EventBus,\n    };\n\n    static template = xml`\n        <t t-if=\"state.blockState === BLOCK_STATES.UNBLOCKED\">\n            <div/>\n        </t>\n        <t t-else=\"\">\n            <t t-set=\"visiblyBlocked\" t-value=\"state.blockState === BLOCK_STATES.VISIBLY_BLOCKED\"/>\n            <div class=\"o_blockUI fixed-top d-flex justify-content-center align-items-center flex-column vh-100\"\n                 t-att-class=\"visiblyBlocked ? '' : 'o_blockUI_invisible'\">\n                <t t-if=\"visiblyBlocked\">\n                    <div class=\"o_spinner mb-4\">\n                        <img src=\"/web/static/img/spin.svg\" alt=\"Loading...\"/>\n                    </div>\n                    <div class=\"o_message text-center px-4\">\n                        <t t-esc=\"state.line1\"/><br/>\n                        <t t-esc=\"state.line2\"/>\n                    </div>\n                </t>\n            </div>\n        </t>\n    `;\n\n    setup() {\n        this.messagesByDuration = [\n            { time: 20, l1: _t(\"Loading...\") },\n            { time: 40, l1: _t(\"Still loading...\") },\n            {\n                time: 60,\n                l1: _t(\"Still loading...\"),\n                l2: _t(\"Please be patient.\"),\n            },\n            {\n                time: 180,\n                l1: _t(\"Don't leave yet,\"),\n                l2: _t(\"it's still loading...\"),\n            },\n            {\n                time: 120,\n                l1: _t(\"You may not believe it,\"),\n                l2: _t(\"but the application is actually loading...\"),\n            },\n            {\n                time: 3180,\n                l1: _t(\"Take a minute to get a coffee,\"),\n                l2: _t(\"because it's loading...\"),\n            },\n            {\n                time: null,\n                l1: _t(\"Maybe you should consider reloading the application by pressing F5...\"),\n            },\n        ];\n        this.BLOCK_STATES = { UNBLOCKED: 0, BLOCKED: 1, VISIBLY_BLOCKED: 2 };\n        this.state = useState({\n            blockState: this.BLOCK_STATES.UNBLOCKED,\n            line1: \"\",\n            line2: \"\",\n        });\n\n        this.props.bus.addEventListener(\"BLOCK\", this.block.bind(this));\n        this.props.bus.addEventListener(\"UNBLOCK\", this.unblock.bind(this));\n    }\n\n    replaceMessage(index) {\n        const message = this.messagesByDuration[index];\n        this.state.line1 = message.l1;\n        this.state.line2 = message.l2 || \"\";\n        if (message.time !== null) {\n            this.msgTimer = browser.setTimeout(() => {\n                this.replaceMessage(index + 1);\n            }, message.time * 1000);\n        }\n    }\n\n    block(ev) {\n        const showBlockedUI = () => (this.state.blockState = this.BLOCK_STATES.VISIBLY_BLOCKED);\n        const delay = ev.detail?.delay;\n        if (delay) {\n            this.state.blockState = this.BLOCK_STATES.BLOCKED;\n            this.showBlockedUITimer = setTimeout(showBlockedUI, delay);\n        } else {\n            showBlockedUI();\n        }\n\n        if (ev.detail?.message) {\n            this.state.line1 = ev.detail.message;\n        } else {\n            this.replaceMessage(0);\n        }\n    }\n\n    unblock() {\n        this.state.blockState = this.BLOCK_STATES.UNBLOCKED;\n        clearTimeout(this.showBlockedUITimer);\n        clearTimeout(this.msgTimer);\n        this.state.line1 = \"\";\n        this.state.line2 = \"\";\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { BlockUI } from \"./block_ui\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { getTabableElements } from \"@web/core/utils/ui\";\nimport { getActiveHotkey } from \"../hotkeys/hotkey_service\";\n\nimport { EventBus, reactive, useEffect, useRef } from \"@odoo/owl\";\n\nexport const SIZES = { XS: 0, VSM: 1, SM: 2, MD: 3, LG: 4, XL: 5, XXL: 6 };\n\nfunction getFirstAndLastTabableElements(el) {\n    const tabableEls = getTabableElements(el);\n    return [tabableEls[0], tabableEls[tabableEls.length - 1]];\n}\n\n/**\n * This hook will set the UI active element\n * when the caller component will mount/patch and\n * only if the t-reffed element has some tabable elements.\n *\n * The caller component could pass a `t-ref` value of its template\n * to delegate the UI active element to another element than itself.\n *\n * @param {string} refName\n */\nexport function useActiveElement(refName) {\n    if (!refName) {\n        throw new Error(\"refName not given to useActiveElement\");\n    }\n    const uiService = useService(\"ui\");\n    const ref = useRef(refName);\n\n    function trapFocus(e) {\n        const hotkey = getActiveHotkey(e);\n        if (![\"tab\", \"shift+tab\"].includes(hotkey)) {\n            return;\n        }\n        const el = e.currentTarget;\n        const [firstTabableEl, lastTabableEl] = getFirstAndLastTabableElements(el);\n        switch (hotkey) {\n            case \"tab\":\n                if (document.activeElement === lastTabableEl) {\n                    firstTabableEl.focus();\n                    e.preventDefault();\n                    e.stopPropagation();\n                }\n                break;\n            case \"shift+tab\":\n                if (document.activeElement === firstTabableEl) {\n                    lastTabableEl.focus();\n                    e.preventDefault();\n                    e.stopPropagation();\n                }\n                break;\n        }\n    }\n\n    useEffect(\n        (el) => {\n            if (el) {\n                const [firstTabableEl] = getFirstAndLastTabableElements(el);\n                if (!firstTabableEl) {\n                    // no tabable elements: no need to trap focus nor become the UI active element\n                    return;\n                }\n                const oldActiveElement = document.activeElement;\n                uiService.activateElement(el);\n\n                el.addEventListener(\"keydown\", trapFocus);\n\n                if (!el.contains(document.activeElement)) {\n                    firstTabableEl.focus();\n                }\n                return () => {\n                    uiService.deactivateElement(el);\n                    el.removeEventListener(\"keydown\", trapFocus);\n\n                    /**\n                     * In some cases, the current active element is not\n                     * anymore in el (e.g. with ConfirmationDialog, the\n                     * confirm button is disabled when clicked, so the\n                     * focus is lost). In that case, we also want to restore\n                     * the focus to the previous active element so we\n                     * check if the current active element is the body\n                     */\n                    if (\n                        el.contains(document.activeElement) ||\n                        document.activeElement === document.body\n                    ) {\n                        oldActiveElement.focus();\n                    }\n                };\n            }\n        },\n        () => [ref.el]\n    );\n}\n\n// window size handling\nexport const MEDIAS_BREAKPOINTS = [\n    { maxWidth: 474 },\n    { minWidth: 475, maxWidth: 575 },\n    { minWidth: 576, maxWidth: 767 },\n    { minWidth: 768, maxWidth: 991 },\n    { minWidth: 992, maxWidth: 1199 },\n    { minWidth: 1200, maxWidth: 1533 },\n    { minWidth: 1534 },\n];\n\n/**\n * Create the MediaQueryList used both by the uiService and config from\n * `MEDIA_BREAKPOINTS`.\n *\n * @returns {MediaQueryList[]}\n */\nexport function getMediaQueryLists() {\n    return MEDIAS_BREAKPOINTS.map(({ minWidth, maxWidth }) => {\n        if (!maxWidth) {\n            return window.matchMedia(`(min-width: ${minWidth}px)`);\n        }\n        if (!minWidth) {\n            return window.matchMedia(`(max-width: ${maxWidth}px)`);\n        }\n        return window.matchMedia(`(min-width: ${minWidth}px) and (max-width: ${maxWidth}px)`);\n    });\n}\n\n// window size handling.\nconst MEDIAS = getMediaQueryLists();\n\nexport const utils = {\n    getSize() {\n        return MEDIAS.findIndex((media) => media.matches);\n    },\n    isSmall(ui = {}) {\n        return (ui.size || utils.getSize()) <= SIZES.SM;\n    },\n};\n\nconst bus = new EventBus();\n\nexport function listenSizeChange(callback) {\n    bus.addEventListener(\"resize\", callback);\n    return () => bus.removeEventListener(\"resize\", callback);\n}\n\nexport const uiService = {\n    start(env) {\n        // block/unblock code\n        registry.category(\"main_components\").add(\"BlockUI\", { Component: BlockUI, props: { bus } });\n\n        let blockCount = 0;\n        function block(data) {\n            blockCount++;\n            // TODO could probably be improved to handle multiple block demands\n            // but that have different messages and delays\n            if (blockCount === 1) {\n                bus.trigger(\"BLOCK\", {\n                    message: data?.message,\n                    delay: data?.delay,\n                });\n            }\n        }\n        function unblock() {\n            blockCount--;\n            if (blockCount < 0) {\n                console.warn(\n                    \"Unblock ui was called more times than block, you should only unblock the UI if you have previously blocked it.\"\n                );\n                blockCount = 0;\n            }\n            if (blockCount === 0) {\n                bus.trigger(\"UNBLOCK\");\n            }\n        }\n\n        // UI active element code\n        let activeElems = [document];\n\n        function activateElement(el) {\n            activeElems.push(el);\n            bus.trigger(\"active-element-changed\", el);\n        }\n        function deactivateElement(el) {\n            activeElems = activeElems.filter((x) => x !== el);\n            bus.trigger(\"active-element-changed\", ui.activeElement);\n        }\n        function getActiveElementOf(el) {\n            for (const activeElement of [...activeElems].reverse()) {\n                if (activeElement.contains(el)) {\n                    return activeElement;\n                }\n            }\n        }\n\n        const ui = reactive({\n            bus,\n            size: utils.getSize(),\n            get activeElement() {\n                return activeElems[activeElems.length - 1];\n            },\n            get isBlocked() {\n                return blockCount > 0;\n            },\n            isSmall: utils.isSmall(),\n            block,\n            unblock,\n            activateElement,\n            deactivateElement,\n            getActiveElementOf,\n        });\n\n        // listen to media query status changes\n        const updateSize = () => {\n            const prevSize = ui.size;\n            ui.size = utils.getSize();\n            if (ui.size !== prevSize) {\n                ui.isSmall = utils.isSmall(ui);\n                bus.trigger(\"resize\");\n            }\n        };\n        browser.addEventListener(\"resize\", throttleForAnimation(updateSize));\n\n        Object.defineProperty(env, \"isSmall\", {\n            get() {\n                return ui.isSmall;\n            },\n        });\n\n        return ui;\n    },\n};\n\nregistry.category(\"services\").add(\"ui\", uiService);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { pyToJsLocale } from \"@web/core/l10n/utils/locales\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { Cache } from \"@web/core/utils/cache\";\nimport { session } from \"@web/session\";\nimport { ensureArray } from \"./utils/arrays\";\n\n// This file exports an object containing user-related information and functions\n// allowing to obtain/alter user-related information from the server.\n\n/**\n * This function exists for testing purposes. We don't want tests to share the\n * same cache. It allows to generate new caches at the beginning of tests.\n *\n * Note: with hoot, this will no longer be necessary.\n *\n * @returns Object\n */\nexport function _makeUser(session) {\n    // Retrieve user-related information from the session\n    const {\n        home_action_id: homeActionId,\n        is_admin: isAdmin,\n        is_internal_user: isInternalUser,\n        is_system: isSystem,\n        name,\n        partner_id: partnerId,\n        show_effect: showEffect,\n        uid: userId,\n        username: login,\n        user_context: context,\n        user_settings,\n        partner_write_date: writeDate,\n    } = session;\n    const settings = user_settings || {};\n\n    // Delete user-related information from the session, s.t. there's a single source of truth\n    delete session.home_action_id;\n    delete session.is_admin;\n    delete session.is_internal_user;\n    delete session.is_system;\n    delete session.name;\n    delete session.partner_id;\n    delete session.show_effect;\n    delete session.uid;\n    delete session.username;\n    delete session.user_context;\n    delete session.user_settings;\n    delete session.partner_write_date;\n\n    // Generate caches for has_group and has_access calls\n    const getGroupCacheValue = (group, context) => {\n        if (!userId) {\n            return Promise.resolve(false);\n        }\n        return rpc(\"/web/dataset/call_kw/res.users/has_group\", {\n            model: \"res.users\",\n            method: \"has_group\",\n            args: [userId, group],\n            kwargs: { context },\n        });\n    };\n    const getGroupCacheKey = (group) => group;\n    const groupCache = new Cache(getGroupCacheValue, getGroupCacheKey);\n    groupCache.cache[\"base.group_user\"] = Promise.resolve(isInternalUser);\n    groupCache.cache[\"base.group_system\"] = Promise.resolve(isSystem);\n    const getAccessRightCacheValue = (model, operation, ids, context) => {\n        const url = `/web/dataset/call_kw/${model}/has_access`;\n        return rpc(url, {\n            model,\n            method: \"has_access\",\n            args: [ids, operation],\n            kwargs: { context },\n        });\n    };\n    const getAccessRightCacheKey = (model, operation, ids) =>\n        JSON.stringify([model, operation, ids]);\n    const accessRightCache = new Cache(getAccessRightCacheValue, getAccessRightCacheKey);\n    const lang = pyToJsLocale(context?.lang);\n\n    const user = {\n        name,\n        login,\n        isAdmin,\n        isSystem,\n        partnerId,\n        homeActionId,\n        showEffect,\n        userId, // TODO: rename into id?\n        writeDate,\n        get context() {\n            return Object.assign({}, context, { uid: this.userId });\n        },\n        get lang() {\n            return lang;\n        },\n        get tz() {\n            return this.context.tz;\n        },\n        get settings() {\n            return Object.assign({}, settings);\n        },\n        updateContext(update) {\n            Object.assign(context, update);\n        },\n        hasGroup(group) {\n            return groupCache.read(group, this.context);\n        },\n        checkAccessRight(model, operation, ids = []) {\n            return accessRightCache.read(model, operation, ensureArray(ids), this.context);\n        },\n        async setUserSettings(key, value) {\n            const model = \"res.users.settings\";\n            const method = \"set_res_users_settings\";\n            const changedSettings = await rpc(`/web/dataset/call_kw/${model}/${method}`, {\n                model,\n                method,\n                args: [[this.settings.id]],\n                kwargs: {\n                    new_settings: {\n                        [key]: value,\n                    },\n                    context: this.context,\n                },\n            });\n            Object.assign(settings, changedSettings);\n        },\n    };\n\n    return user;\n}\n\nexport const user = _makeUser(session);\n\nconst LAST_CONNECTED_USER_KEY = \"web.lastConnectedUser\";\n\nexport const getLastConnectedUsers = () => {\n    const lastConnectedUsers = browser.localStorage.getItem(LAST_CONNECTED_USER_KEY);\n    return lastConnectedUsers ? JSON.parse(lastConnectedUsers) : [];\n};\n\nexport const setLastConnectedUsers = (users) => {\n    browser.localStorage.setItem(LAST_CONNECTED_USER_KEY, JSON.stringify(users.slice(0, 5)));\n};\n\nif (user.login && user.login !== \"__system__\") {\n    const users = getLastConnectedUsers();\n    const lastConnectedUsers = [\n        {\n            login: user.login,\n            name: user.name,\n            partnerId: user.partnerId,\n            partnerWriteDate: user.writeDate,\n            userId: user.userId,\n        },\n        ...users.filter((u) => u.userId !== user.userId),\n    ];\n    setLastConnectedUsers(lastConnectedUsers);\n}\n", "import { Component, useRef, useState, useEffect } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { getLastConnectedUsers, setLastConnectedUsers } from \"@web/core/user\";\nimport { imageUrl } from \"@web/core/utils/urls\";\n\nexport class UserSwitch extends Component {\n    static template = \"web.login_user_switch\";\n    static props = {};\n\n    setup() {\n        const users = getLastConnectedUsers();\n        this.root = useRef(\"root\");\n        this.state = useState({\n            users,\n            displayUserChoice: users.length > 1,\n        });\n        this.form = document.querySelector(\"form.oe_login_form\");\n        this.form.classList.toggle(\"d-none\", users.length > 1);\n        this.form.querySelector(\":placeholder-shown\")?.focus();\n        useEffect(\n            (el) => el?.querySelector(\"button.list-group-item-action\")?.focus(),\n            () => [this.root.el]\n        );\n    }\n\n    toggleFormDisplay() {\n        this.state.displayUserChoice = !this.state.displayUserChoice && this.state.users.length;\n        this.form.classList.toggle(\"d-none\", this.state.displayUserChoice);\n        this.form.querySelector(\":placeholder-shown\")?.focus();\n    }\n\n    getAvatarUrl({ partnerId, partnerWriteDate: unique }) {\n        return imageUrl(\"res.partner\", partnerId, \"avatar_128\", { unique });\n    }\n\n    remove(deletedUser) {\n        this.state.users = this.state.users.filter((user) => user !== deletedUser);\n        setLastConnectedUsers(this.state.users);\n        if (!this.state.users.length) {\n            this.fillForm();\n        }\n    }\n\n    fillForm(login = \"\") {\n        this.form.querySelector(\"input#login\").value = login;\n        this.form.querySelector(\"input#password\").value = \"\";\n        this.toggleFormDisplay();\n    }\n}\n\nregistry.category(\"public_components\").add(\"web.user_switch\", UserSwitch);\n", "import { shallowEqual as _shallowEqual } from \"./objects\";\n\n/**\n * @template T\n * @template {string | number | symbol} K\n * @typedef {keyof T | ((item: T) => K)} Criterion\n */\n\n/**\n * Same values returned as those returned by cartesian function for case n = 0\n * and n > 1. For n = 1, brackets are put around the unique parameter elements.\n *\n * @template T\n * @param {...T[]} args\n * @returns {T[][]}\n */\nfunction _cartesian(...args) {\n    if (args.length === 0) {\n        return [undefined];\n    }\n    const firstArray = args.shift().map((elem) => [elem]);\n    if (args.length === 0) {\n        return firstArray;\n    }\n    const result = [];\n    const productOfOtherArrays = _cartesian(...args);\n    for (const array of firstArray) {\n        for (const tuple of productOfOtherArrays) {\n            result.push([...array, ...tuple]);\n        }\n    }\n    return result;\n}\n\n/**\n * Helper function returning an extraction handler to use on array elements to\n * return a certain attribute or mutated form of the element.\n *\n * @private\n * @template T\n * @template {string | number | symbol} K\n * @param {Criterion<T, K>} [criterion]\n * @returns {(element: T) => any}\n */\nfunction _getExtractorFrom(criterion) {\n    if (criterion) {\n        switch (typeof criterion) {\n            case \"string\":\n                return (element) => element[criterion];\n            case \"function\":\n                return criterion;\n            default:\n                throw new Error(\n                    `Expected criterion of type 'string' or 'function' and got '${typeof criterion}'`\n                );\n        }\n    } else {\n        return (element) => element;\n    }\n}\n\n/**\n * Returns an array containing either:\n * - the elements contained in the given iterable OR\n * - the given element if it is not an iterable\n *\n * @template T\n * @param {T | Iterable<T>} [value]\n * @returns {T[]}\n */\nexport function ensureArray(value) {\n    return isIterable(value) ? [...value] : [value];\n}\n\n/**\n * Returns the array of elements contained in both arrays.\n *\n * @template T\n * @param {Iterable<T>} iter1\n * @param {Iterable<T>} iter2\n * @returns {T[]}\n */\nexport function intersection(iter1, iter2) {\n    const set2 = new Set(iter2);\n    return unique(iter1).filter((v) => set2.has(v));\n}\n\n/**\n * Returns whether the given value is an iterable object (excluding strings).\n *\n * @param {unknown} value\n */\nexport function isIterable(value) {\n    return Boolean(value && typeof value === \"object\" && value[Symbol.iterator]);\n}\n\n/**\n * Returns an object holding different groups defined by a given criterion\n * or a default one. Each group is a subset of the original given list.\n * The given criterion can either be:\n * - a string: a property name on the list elements which value will be the\n * group name,\n * - a function: a handler that will return the group name from a given\n * element.\n *\n * @template T\n * @template {string | number | symbol} K\n * @param {Iterable<T>} iterable\n * @param {Criterion<T, K>} [criterion]\n * @returns {Record<K, T[]>}\n */\nexport function groupBy(iterable, criterion) {\n    const extract = _getExtractorFrom(criterion);\n    /** @type {Partial<Record<K, T[]>>} */\n    const groups = {};\n    for (const element of iterable) {\n        const group = String(extract(element));\n        if (!(group in groups)) {\n            groups[group] = [];\n        }\n        groups[group].push(element);\n    }\n    return groups;\n}\n\n/**\n * Return a shallow copy of a given array sorted by a given criterion or a default one.\n * The given criterion can either be:\n * - a string: a property name on the array elements returning the sortable primitive\n * - a function: a handler that will return the sortable primitive from a given element.\n * The default order is ascending ('asc'). It can be modified by setting the extra param 'order' to 'desc'.\n *\n * @template T\n * @template {string | number | symbol} K\n * @param {Iterable<T>} iterable\n * @param {Criterion<T, K>} [criterion]\n * @param {\"asc\" | \"desc\"} [order=\"asc\"]\n * @returns {T[]}\n */\nexport function sortBy(iterable, criterion, order = \"asc\") {\n    const extract = _getExtractorFrom(criterion);\n    return [...iterable].sort((elA, elB) => {\n        const a = extract(elA);\n        const b = extract(elB);\n        let result;\n        if (isNaN(a) && isNaN(b)) {\n            result = a > b ? 1 : a < b ? -1 : 0;\n        } else {\n            result = a - b;\n        }\n        return order === \"asc\" ? result : -result;\n    });\n}\n\n/**\n * Returns an array containing all the elements of arrayA\n * that are not in arrayB and vice-versa.\n *\n * @template T\n * @param {Iterable<T>} iter1\n * @param {Iterable<T>} iter2\n * @returns {T[]} an array containing all the elements of iter1\n * that are not in iter2 and vice-versa.\n */\nexport function symmetricalDifference(iter1, iter2) {\n    const array1 = [...iter1];\n    const array2 = [...iter2];\n    return [\n        ...array1.filter((value) => !array2.includes(value)),\n        ...array2.filter((value) => !array1.includes(value)),\n    ];\n}\n\n/**\n * Returns the product of any number n of arrays.\n * The internal structures of their elements is preserved.\n * For n = 1, no brackets are put around the unique parameter elements\n * For n = 0, [undefined] is returned since it is the unit\n * of the cartesian product (up to isomorphism).\n *\n * @template T\n * @param {...T[]} args\n * @returns {T[] | T[][]}\n */\nexport function cartesian(...args) {\n    if (args.length === 0) {\n        return [undefined];\n    } else if (args.length === 1) {\n        return args[0];\n    } else {\n        return _cartesian(...args);\n    }\n}\n\nexport const shallowEqual = _shallowEqual;\n\n/**\n * Returns all initial sections of a given array, e.g. for [1, 2] the array\n * [[], [1], [1, 2]] is returned.\n *\n * @template T\n * @param {Iterable<T>} iterable\n * @returns {T[][]}\n */\nexport function sections(iterable) {\n    const array = [...iterable];\n    const sections = [];\n    for (let i = 0; i < array.length + 1; i++) {\n        sections.push(array.slice(0, i));\n    }\n    return sections;\n}\n\n/**\n * Returns an array containing all elements of the given\n * array but without duplicates.\n *\n * @template T\n * @param {Iterable<T>} iterable\n * @returns {T[]}\n */\nexport function unique(iterable) {\n    return [...new Set(iterable)];\n}\n\n/**\n * @template T1, T2\n * @param {Iterable<T1>} iter1\n * @param {Iterable<T2>} iter2\n * @param {boolean} [fill=false]\n * @returns {[T1, T2][]}\n */\nexport function zip(iter1, iter2, fill = false) {\n    const array1 = [...iter1];\n    const array2 = [...iter2];\n    /** @type {[T1, T2][]} */\n    const result = [];\n    const getLength = fill ? Math.max : Math.min;\n    for (let i = 0; i < getLength(array1.length, array2.length); i++) {\n        result.push([array1[i], array2[i]]);\n    }\n    return result;\n}\n\n/**\n * @template T1, T2, T\n * @param {Iterable<T1>} iter1\n * @param {Iterable<T2>} iter2\n * @param {(e1: T1, e2: T2) => T} mapFn\n * @returns {T[]}\n */\nexport function zipWith(iter1, iter2, mapFn) {\n    return zip(iter1, iter2).map(([e1, e2]) => mapFn(e1, e2));\n}\n/**\n * Creates an sliding window over an array of a given width. Eg:\n * slidingWindow([1, 2, 3, 4], 2) => [[1, 2], [2, 3], [3, 4]]\n *\n * @template T\n * @param {T[]} arr the array over which to create a sliding window\n * @param {number} width the width of the window\n * @returns {T[][]} an array of tuples of size width\n */\nexport function slidingWindow(arr, width) {\n    const res = [];\n    for (let i = 0; i <= arr.length - width; i++) {\n        res.push(arr.slice(i, i + width));\n    }\n    return res;\n}\n\nexport function rotate(i, arr, inc = 1) {\n    return (arr.length + i + inc) % arr.length;\n}\n", "import { useEffect } from \"@odoo/owl\";\nimport { browser } from \"../browser/browser\";\n\n/**\n * This is used on text inputs or textareas to automatically resize it based on its\n * content each time it is updated. It takes the reference of the element as\n * parameter and some options. Do note that it may introduce mild performance issues\n * since it will force a reflow of the layout each time the element is updated.\n * Do also note that it only works with textareas that are nested as only child\n * of some parent div (like in the text_field component).\n *\n * @param {Ref} ref\n */\nexport function useAutoresize(ref, options = {}) {\n    let wasProgrammaticallyResized = false;\n    let resize = null;\n    useEffect(\n        (el) => {\n            if (el) {\n                resize = (programmaticResize = false) => {\n                    wasProgrammaticallyResized = programmaticResize;\n                    if (el instanceof HTMLInputElement) {\n                        resizeInput(el, options);\n                    } else {\n                        resizeTextArea(el, options);\n                    }\n                    options.onResize?.(el, options);\n                };\n                el.addEventListener(\"input\", () => resize(true));\n                const resizeObserver = new ResizeObserver(() => {\n                    // This ensures that the resize function is not called twice on input or page load\n                    if (wasProgrammaticallyResized) {\n                        wasProgrammaticallyResized = false;\n                        return;\n                    }\n                    resize();\n                });\n                resizeObserver.observe(el);\n                return () => {\n                    el.removeEventListener(\"input\", resize);\n                    resizeObserver.unobserve(el);\n                    resizeObserver.disconnect();\n                    resize = null;\n                };\n            }\n        },\n        () => [ref.el]\n    );\n    useEffect(() => {\n        if (resize) {\n            resize(true);\n        }\n    });\n}\n\nfunction resizeInput(input) {\n    // This mesures the maximum width of the input which can get from the flex layout.\n    input.style.width = \"100%\";\n    const maxWidth = input.clientWidth;\n    // Somehow Safari 16 computes input sizes incorrectly. This is fixed in Safari 17\n    const isSafari16 = /Version\\/16.+Safari/i.test(browser.navigator.userAgent);\n    // Minimum width of the input\n    input.style.width = \"10px\";\n    if (input.value === \"\" && input.placeholder !== \"\") {\n        input.style.width = \"auto\";\n        return;\n    }\n    if (input.scrollWidth + 5 + (isSafari16 ? 8 : 0) > maxWidth) {\n        input.style.width = \"100%\";\n        return;\n    }\n    input.style.width = input.scrollWidth + 5 + (isSafari16 ? 8 : 0) + \"px\";\n}\n\nexport function resizeTextArea(textarea, options = {}) {\n    const minimumHeight = options.minimumHeight || 0;\n    let heightOffset = 0;\n    const style = window.getComputedStyle(textarea);\n    if (style.boxSizing === \"border-box\") {\n        const paddingHeight = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom);\n        const borderHeight = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);\n        heightOffset = borderHeight + paddingHeight;\n    }\n    const previousStyle = {\n        borderTopWidth: style.borderTopWidth,\n        borderBottomWidth: style.borderBottomWidth,\n        padding: style.padding,\n    };\n    Object.assign(textarea.style, {\n        height: \"auto\",\n        borderTopWidth: 0,\n        borderBottomWidth: 0,\n        paddingTop: 0,\n        paddingRight: style.paddingRight,\n        paddingBottom: 0,\n        paddingLeft: style.paddingLeft,\n    });\n    textarea.style.height = \"auto\";\n    const height = Math.max(minimumHeight, textarea.scrollHeight + heightOffset);\n    Object.assign(textarea.style, previousStyle, { height: `${height}px` });\n    textarea.parentElement.style.height = `${height}px`;\n}\n", "import { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @param {string} value\n * @returns {boolean}\n */\nexport function isBinarySize(value) {\n    return /^\\d+(\\.\\d*)? [^0-9]+$/.test(value);\n}\n\n/**\n * Get the length necessary for a base64 str to encode maxBytes\n * @param {number} maxBytes number of bytes we want to encode in base64\n * @returns {number} number of char\n */\nexport function toBase64Length(maxBytes) {\n    return Math.ceil(maxBytes * 4 / 3);\n}\n\n/**\n * @param {number} size number of bytes\n * @param {string}\n */\nexport function humanSize(size) {\n    const units = _t(\"Bytes|Kb|Mb|Gb|Tb|Pb|Eb|Zb|Yb\").split(\"|\");\n    let i = 0;\n    while (size >= 1024) {\n        size /= 1024;\n        ++i;\n    }\n    return `${size.toFixed(2)} ${units[i].trim()}`;\n}\n", "export class Cache {\n    constructor(getValue, getKey) {\n        this.cache = {};\n        this.getKey = getKey;\n        this.getValue = getValue;\n    }\n    _getCacheAndKey(...path) {\n        let cache = this.cache;\n        let key;\n        if (this.getKey) {\n            key = this.getKey(...path);\n        } else {\n            for (let i = 0; i < path.length - 1; i++) {\n                cache = cache[path[i]] = cache[path[i]] || {};\n            }\n            key = path[path.length - 1];\n        }\n        return { cache, key };\n    }\n    clear(...path) {\n        const { cache, key } = this._getCacheAndKey(...path);\n        delete cache[key];\n    }\n    invalidate() {\n        this.cache = {};\n    }\n    read(...path) {\n        const { cache, key } = this._getCacheAndKey(...path);\n        if (!(key in cache)) {\n            cache[key] = this.getValue(...path);\n        }\n        return cache[key];\n    }\n}\n", "/**\n * Adds the given classes to an element, whether the classes\n * are strings or objects.\n *\n * @param {HTMLElement} el\n * @param {String|Object|undefined} classes\n *\n * @example\n * addClassesToElement(el, \"hello\", { \"world\": 0 == 1, }...)\n */\nexport function addClassesToElement(el, ...classes) {\n    for (const classDefinition of classes) {\n        const classObj = toClassObj(classDefinition);\n        for (const className in classObj) {\n            if (classObj[className]) {\n                el.classList.add(className.trim());\n            }\n        }\n    }\n}\n\n/**\n * Merges two classes to a single class object, whether the\n * classes are strings or objects.\n *\n * @param {String|Object|undefined} classes\n * @returns {Object}\n *\n * @example\n * mergeClasses(\"hello\", { \"world\": 0 == 1, }...)\n */\nexport function mergeClasses(...classes) {\n    const classObj = {};\n    for (const classDefinition of classes) {\n        Object.assign(classObj, toClassObj(classDefinition));\n    }\n    return classObj;\n}\n\n/**\n * Returns an object from a class definition, whether it\n * is a string or an object.\n *\n * The returned object keys are css class names and the\n * values are expressions which represent if the class\n * should be added or not.\n *\n * @param {String|Object|undefined} classDefinition\n * @returns {Object}\n */\nfunction toClassObj(classDefinition) {\n    if (!classDefinition) {\n        return {};\n    } else if (typeof classDefinition === \"object\") {\n        return classDefinition;\n    } else if (typeof classDefinition === \"string\") {\n        const classObj = {};\n        classDefinition\n            .trim()\n            .split(/\\s+/)\n            .forEach((s) => {\n                classObj[s] = true;\n            });\n        return classObj;\n    } else {\n        console.warn(\n            `toClassObj only supports strings, objects and undefined className (got ${typeof classProp})`\n        );\n        return {};\n    }\n}\n", "/**\n * Converts RGB color components to HSL components.\n *\n * @static\n * @param {integer} r - [0, 255]\n * @param {integer} g - [0, 255]\n * @param {integer} b - [0, 255]\n * @returns {Object|false}\n *          - hue [0, 360[ (float)\n *          - saturation [0, 100] (float)\n *          - lightness [0, 100] (float)\n */\nexport function convertRgbToHsl(r, g, b) {\n    if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255\n            || typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255\n            || typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) {\n        return false;\n    }\n\n    var red = r / 255;\n    var green = g / 255;\n    var blue = b / 255;\n    var maxColor = Math.max(red, green, blue);\n    var minColor = Math.min(red, green, blue);\n    var delta = maxColor - minColor;\n    var hue = 0;\n    var saturation = 0;\n    var lightness = (maxColor + minColor) / 2;\n    if (delta) {\n        if (maxColor === red) {\n            hue = (green - blue) / delta;\n        }\n        if (maxColor === green) {\n            hue = 2 + (blue - red) / delta;\n        }\n        if (maxColor === blue) {\n            hue = 4 + (red - green) / delta;\n        }\n        if (maxColor) {\n            saturation = delta / (1 - Math.abs(2 * lightness - 1));\n        }\n    }\n    hue = 60 * hue;\n    return {\n        hue: hue < 0 ? hue + 360 : hue,\n        saturation: saturation * 100,\n        lightness: lightness * 100,\n    };\n};\n/**\n * Converts HSL color components to RGB components.\n *\n * @static\n * @param {number} h - [0, 360[ (float)\n * @param {number} s - [0, 100] (float)\n * @param {number} l - [0, 100] (float)\n * @returns {Object|false}\n *          - red [0, 255] (integer)\n *          - green [0, 255] (integer)\n *          - blue [0, 255] (integer)\n */\nexport function convertHslToRgb(h, s, l) {\n    if (typeof (h) !== 'number' || isNaN(h) || h < 0 || h > 360\n            || typeof (s) !== 'number' || isNaN(s) || s < 0 || s > 100\n            || typeof (l) !== 'number' || isNaN(l) || l < 0 || l > 100) {\n        return false;\n    }\n\n    var huePrime = h / 60;\n    var saturation = s / 100;\n    var lightness = l / 100;\n    var chroma = saturation * (1 - Math.abs(2 * lightness - 1));\n    var secondComponent = chroma * (1 - Math.abs(huePrime % 2 - 1));\n    var lightnessAdjustment = lightness - chroma / 2;\n    var precision = 255;\n    chroma = Math.round((chroma + lightnessAdjustment) * precision);\n    secondComponent = Math.round((secondComponent + lightnessAdjustment) * precision);\n    lightnessAdjustment = Math.round((lightnessAdjustment) * precision);\n    if (huePrime >= 0 && huePrime < 1) {\n        return {\n            red: chroma,\n            green: secondComponent,\n            blue: lightnessAdjustment,\n        };\n    }\n    if (huePrime >= 1 && huePrime < 2) {\n        return {\n            red: secondComponent,\n            green: chroma,\n            blue: lightnessAdjustment,\n        };\n    }\n    if (huePrime >= 2 && huePrime < 3) {\n        return {\n            red: lightnessAdjustment,\n            green: chroma,\n            blue: secondComponent,\n        };\n    }\n    if (huePrime >= 3 && huePrime < 4) {\n        return {\n            red: lightnessAdjustment,\n            green: secondComponent,\n            blue: chroma,\n        };\n    }\n    if (huePrime >= 4 && huePrime < 5) {\n        return {\n            red: secondComponent,\n            green: lightnessAdjustment,\n            blue: chroma,\n        };\n    }\n    if (huePrime >= 5 && huePrime <= 6) {\n        return {\n            red: chroma,\n            green: lightnessAdjustment,\n            blue: secondComponent,\n        };\n    }\n    return false;\n};\n/**\n * Converts RGBA color components to a normalized CSS color: if the opacity\n * is invalid or equal to 100, a hex is returned; otherwise a rgba() css color\n * is returned.\n *\n * Those choice have multiple reason:\n * - A hex color is more common to c/c from other utilities on the web and is\n *   also shorter than rgb() css colors\n * - Opacity in hexadecimal notations is not supported on all browsers and is\n *   also less common to use.\n *\n * @static\n * @param {integer} r - [0, 255]\n * @param {integer} g - [0, 255]\n * @param {integer} b - [0, 255]\n * @param {float} a - [0, 100]\n * @returns {string}\n */\nexport function convertRgbaToCSSColor(r, g, b, a) {\n    if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255\n            || typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255\n            || typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) {\n        return false;\n    }\n    if (typeof (a) !== 'number' || isNaN(a) || a < 0 || Math.abs(a - 100) < Number.EPSILON) {\n        const rr = r < 16 ? '0' + r.toString(16) : r.toString(16);\n        const gg = g < 16 ? '0' + g.toString(16) : g.toString(16);\n        const bb = b < 16 ? '0' + b.toString(16) : b.toString(16);\n        return (`#${rr}${gg}${bb}`).toUpperCase();\n    }\n    return `rgba(${r}, ${g}, ${b}, ${parseFloat((a / 100.0).toFixed(3))})`;\n};\n/**\n * Converts a CSS color (rgb(), rgba(), hexadecimal) to RGBA color components.\n *\n * Note: we don't support using and displaying hexadecimal color with opacity\n * but this method allows to receive one and returns the correct opacity value.\n *\n * @static\n * @param {string} cssColor - hexadecimal code or rgb() or rgba() or color()\n * @returns {Object|false}\n *          - red [0, 255] (integer)\n *          - green [0, 255] (integer)\n *          - blue [0, 255] (integer)\n *          - opacity [0, 100.0] (float)\n */\nexport function convertCSSColorToRgba(cssColor) {\n    // Check if cssColor is a rgba() or rgb() color\n    const rgba = cssColor.match(/^rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*(\\d+(?:\\.\\d+)?))?\\)$/);\n    if (rgba) {\n        if (rgba[4] === undefined) {\n            rgba[4] = 1;\n        }\n        return {\n            red: parseInt(rgba[1]),\n            green: parseInt(rgba[2]),\n            blue: parseInt(rgba[3]),\n            opacity: Math.round(parseFloat(rgba[4]) * 100),\n        };\n    }\n\n    // Otherwise, check if cssColor is an hexadecimal code color\n    if (/^#([0-9A-F]{6}|[0-9A-F]{8})$/i.test(cssColor)) {\n        return {\n            red: parseInt(cssColor.substr(1, 2), 16),\n            green: parseInt(cssColor.substr(3, 2), 16),\n            blue: parseInt(cssColor.substr(5, 2), 16),\n            opacity: (cssColor.length === 9 ? (parseInt(cssColor.substr(7, 2), 16) / 255) : 1) * 100,\n        };\n    }\n\n    // TODO maybe implement a support for receiving css color like 'red' or\n    // 'transparent' (which are now considered non-css color by isCSSColor...)\n    // Note: however, if ever implemented be careful of 'white'/'black' which\n    // actually are color names for our color system...\n\n    // Check if cssColor is a color() functional notation allowing colorspace\n    // with implicit sRGB.\n    // \"<color()>\" allows to define a color specification in a formalized\n    // manner. It starts with the \"color(\" keyword, specifies color space\n    // parameters, and optionally includes an alpha value for transparency.\n    if (/color\\(.+\\)/.test(cssColor)) {\n        const canvasEl = document.createElement(\"canvas\");\n        canvasEl.height = 1;\n        canvasEl.width = 1;\n        const ctx = canvasEl.getContext(\"2d\");\n        ctx.fillStyle = cssColor;\n        ctx.fillRect(0, 0, 1, 1);\n        const data = ctx.getImageData(0, 0, 1, 1).data;\n        return {\n            red: data[0],\n            green: data[1],\n            blue: data[2],\n            opacity: data[3] / 2.55, // Convert 0-255 to percentage\n        };\n    }\n    return false;\n};\n/**\n * Converts a CSS color (rgb(), rgba(), hexadecimal) to a normalized version\n * of the same color (@see convertRgbaToCSSColor).\n *\n * Normalized color can be safely compared using string comparison.\n *\n * @static\n * @param {string} cssColor - hexadecimal code or rgb() or rgba()\n * @returns {string} - the normalized css color or the given css color if it\n *                     failed to be normalized\n */\nexport function normalizeCSSColor(cssColor) {\n    const rgba = convertCSSColorToRgba(cssColor);\n    if (!rgba) {\n        return cssColor;\n    }\n    return convertRgbaToCSSColor(rgba.red, rgba.green, rgba.blue, rgba.opacity);\n};\n/**\n * Checks if a given string is a css color.\n *\n * @static\n * @param {string} cssColor\n * @returns {boolean}\n */\nexport function isCSSColor(cssColor) {\n    return convertCSSColorToRgba(cssColor) !== false;\n};\n/**\n * Mixes two colors by applying a weighted average of their red, green and blue\n * components.\n *\n * @static\n * @param {string} cssColor1 - hexadecimal code or rgb() or rgba()\n * @param {string} cssColor2 - hexadecimal code or rgb() or rgba()\n * @param {number} weight - a number between 0 and 1\n * @returns {string} - mixed color in hexadecimal format\n */\nexport function mixCssColors(cssColor1, cssColor2, weight) {\n    const rgba1 = convertCSSColorToRgba(cssColor1);\n    const rgba2 = convertCSSColorToRgba(cssColor2);\n    const rgb1 = [rgba1.red, rgba1.green, rgba1.blue];\n    const rgb2 = [rgba2.red, rgba2.green, rgba2.blue];\n    const [r, g, b] = rgb1.map((_, idx) => Math.round(rgb2[idx] + (rgb1[idx] - rgb2[idx]) * weight));\n    return convertRgbaToCSSColor(r, g, b);\n};\n", "import { Component, onError, xml } from \"@odoo/owl\";\n\nexport class ErrorHandler extends Component {\n    static template = xml`<t t-slot=\"default\" />`;\n    static props = [\"onError\", \"slots\"];\n    setup() {\n        onError((error) => {\n            this.props.onError(error);\n        });\n    }\n}\n", "/**\n * Returns a promise resolved after 'wait' milliseconds\n *\n * @param {int} [wait=0] the delay in ms\n * @return {Promise}\n */\nexport function delay(wait) {\n    return new Promise(function (resolve) {\n        setTimeout(resolve, wait);\n    });\n}\n\n/**\n * KeepLast is a concurrency primitive that manages a list of tasks, and only\n * keeps the last task active.\n *\n * @template T\n */\nexport class KeepLast {\n    constructor() {\n        this._id = 0;\n    }\n    /**\n     * Register a new task\n     *\n     * @param {Promise<T>} promise\n     * @returns {Promise<T>}\n     */\n    add(promise) {\n        this._id++;\n        const currentId = this._id;\n        return new Promise((resolve, reject) => {\n            promise\n                .then((value) => {\n                    if (this._id === currentId) {\n                        resolve(value);\n                    }\n                })\n                .catch((reason) => {\n                    // not sure about this part\n                    if (this._id === currentId) {\n                        reject(reason);\n                    }\n                });\n        });\n    }\n}\n\n/**\n * A (Odoo) mutex is a primitive for serializing computations.  This is\n * useful to avoid a situation where two computations modify some shared\n * state and cause some corrupted state.\n *\n * Imagine that we have a function to fetch some data _load(), which returns\n * a promise which resolves to something useful. Now, we have some code\n * looking like this::\n *\n *      return this._load().then(function (result) {\n *          this.state = result;\n *      });\n *\n * If this code is run twice, but the second execution ends before the\n * first, then the final state will be the result of the first call to\n * _load.  However, if we have a mutex::\n *\n *      this.mutex = new Mutex();\n *\n * and if we wrap the calls to _load in a mutex::\n *\n *      return this.mutex.exec(function() {\n *          return this._load().then(function (result) {\n *              this.state = result;\n *          });\n *      });\n *\n * Then, it is guaranteed that the final state will be the result of the\n * second execution.\n *\n * A Mutex has to be a class, and not a function, because we have to keep\n * track of some internal state.\n */\nexport class Mutex {\n    constructor() {\n        this._lock = Promise.resolve();\n        this._queueSize = 0;\n        this._unlockedProm = undefined;\n        this._unlock = undefined;\n    }\n    /**\n     * Add a computation to the queue, it will be executed as soon as the\n     * previous computations are completed.\n     *\n     * @param {() => (void | Promise<void>)} action a function which may return a Promise\n     * @returns {Promise<void>}\n     */\n    async exec(action) {\n        this._queueSize++;\n        if (!this._unlockedProm) {\n            this._unlockedProm = new Promise((resolve) => {\n                this._unlock = () => {\n                    resolve();\n                    this._unlockedProm = undefined;\n                };\n            });\n        }\n        const always = () => {\n            return Promise.resolve(action()).finally(() => {\n                if (--this._queueSize === 0) {\n                    this._unlock();\n                }\n            });\n        };\n        this._lock = this._lock.then(always, always);\n        return this._lock;\n    }\n    /**\n     * @returns {Promise<void>} resolved as soon as the Mutex is unlocked\n     *   (directly if it is currently idle)\n     */\n    getUnlockedDef() {\n        return this._unlockedProm || Promise.resolve();\n    }\n}\n\n/**\n * Race is a class designed to manage concurrency problems inspired by\n * Promise.race(), except that it is dynamic in the sense that promises can be\n * added anytime to a Race instance. When a promise is added, it returns another\n * promise which resolves as soon as a promise, among all added promises, is\n * resolved. The race is thus over. From that point, a new race will begin the\n * next time a promise will be added.\n *\n * @template T\n */\nexport class Race {\n    constructor() {\n        this.currentProm = null;\n        this.currentPromResolver = null;\n        this.currentPromRejecter = null;\n    }\n    /**\n     * Register a new promise. If there is an ongoing race, the promise is added\n     * to that race. Otherwise, it starts a new race. The returned promise\n     * resolves as soon as the race is over, with the value of the first resolved\n     * promise added to the race.\n     *\n     * @param {Promise<T>} promise\n     * @returns {Promise<T>}\n     */\n    add(promise) {\n        if (!this.currentProm) {\n            this.currentProm = new Promise((resolve, reject) => {\n                this.currentPromResolver = (value) => {\n                    this.currentProm = null;\n                    this.currentPromResolver = null;\n                    this.currentPromRejecter = null;\n                    resolve(value);\n                };\n                this.currentPromRejecter = (error) => {\n                    this.currentProm = null;\n                    this.currentPromResolver = null;\n                    this.currentPromRejecter = null;\n                    reject(error);\n                };\n            });\n        }\n        promise.then(this.currentPromResolver).catch(this.currentPromRejecter);\n        return this.currentProm;\n    }\n    /**\n     * @returns {Promise<T>|null} promise resolved as soon as the race is over, or\n     *   null if there is no race ongoing)\n     */\n    getCurrentProm() {\n        return this.currentProm;\n    }\n}\n\n/**\n * Deferred is basically a resolvable/rejectable extension of Promise.\n */\nexport class Deferred extends Promise {\n    constructor() {\n        let resolve;\n        let reject;\n        const prom = new Promise((res, rej) => {\n            resolve = res;\n            reject = rej;\n        });\n        return Object.assign(prom, { resolve, reject });\n    }\n}\n", "import { makeDraggableHook } from \"@web/core/utils/draggable_hook_builder_owl\";\nimport { pick } from \"@web/core/utils/objects\";\n\n/** @typedef {import(\"@web/core/utils/draggable_hook_builder\").DraggableHandlerParams} DraggableHandlerParams */\n\n/**\n * @typedef DraggableParams\n *\n * MANDATORY\n *\n * @property {{ el: HTMLElement | null }} ref\n * @property {string} elements defines draggable elements\n *\n * OPTIONAL\n *\n * @property {boolean | () => boolean} [enable] whether the draggable system should\n *  be enabled.\n * @property {string | () => string} [handle] additional selector for when the dragging\n *  sequence must be initiated when dragging on a certain part of the element.\n * @property {string | () => string} [ignore] selector targetting elements that must\n *  initiate a drag.\n * @property {string | () => string} [cursor] cursor style during the dragging sequence.\n *\n * HANDLERS (also optional)\n *\n * @property {(params: DraggableHandlerParams) => any} [onDragStart]\n *  called when a dragging sequence is initiated.\n * @property {(params: DraggableHandlerParams) => any} [onDrag]\n *  called on each \"mousemove\" during the drag sequence.\n * @property {(params: DraggableHandlerParams) => any} [onDragEnd]\n *  called when the dragging sequence ends, regardless of the reason.\n * @property {(params: DraggableHandlerParams) => any} [onDrop] called when the dragging sequence\n *  ends on a mouseup action.\n */\n\n/**\n * @typedef DraggableState\n * @property {boolean} dragging\n */\n\n/** @type {(params: DraggableParams) => DraggableState} */\nexport const useDraggable = makeDraggableHook({\n    name: \"useDraggable\",\n    onWillStartDrag: ({ ctx }) => pick(ctx.current, \"element\"),\n    onDragStart: ({ ctx }) => pick(ctx.current, \"element\"),\n    onDrag: ({ ctx }) => pick(ctx.current, \"element\"),\n    onDragEnd: ({ ctx }) => pick(ctx.current, \"element\"),\n    onDrop: ({ ctx }) => pick(ctx.current, \"element\"),\n});\n", "import { clamp } from \"@web/core/utils/numbers\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { closestScrollableX, closestScrollableY } from \"@web/core/utils/scrolling\";\nimport { setRecurringAnimationFrame } from \"@web/core/utils/timing\";\nimport { browser } from \"../browser/browser\";\nimport { hasTouch, isBrowserFirefox, isIOS } from \"../browser/feature_detection\";\n\n/**\n * @typedef {ReturnType<typeof makeCleanupManager>} CleanupManager\n *\n * @typedef {ReturnType<typeof makeDOMHelpers>} DOMHelpers\n *\n * @typedef DraggableBuilderParams\n * Hook params\n * @property {string} [name=\"useAnonymousDraggable\"]\n * @property {EdgeScrollingOptions} [edgeScrolling]\n * @property {Record<string, string[]>} [acceptedParams]\n * @property {Record<string, any>} [defaultParams]\n * Setup hooks\n * @property {{\n *  addListener: typeof import(\"@odoo/owl\")[\"useExternalListener\"];\n *  setup: typeof import(\"@odoo/owl\")[\"useEffect\"];\n *  teardown: typeof import(\"@odoo/owl\")[\"onWillUnmount\"];\n *  throttle: typeof import(\"./timing\")[\"useThrottleForAnimation\"];\n *  wrapState: typeof import(\"@odoo/owl\")[\"reactive\"];\n * }} setupHooks\n * Build hooks\n * @property {(params: DraggableBuildHandlerParams) => any} onComputeParams\n * Runtime hooks\n * @property {(params: DraggableBuildHandlerParams) => any} onDragStart\n * @property {(params: DraggableBuildHandlerParams) => any} onDrag\n * @property {(params: DraggableBuildHandlerParams) => any} onDragEnd\n * @property {(params: DraggableBuildHandlerParams) => any} onDrop\n * @property {(params: DraggableBuildHandlerParams) => any} onWillStartDrag\n *\n * @typedef DraggableHookContext\n * @property {{ el: HTMLElement | null }} ref\n * @property {string | null} [elementSelector=null]\n * @property {string | null} [ignoreSelector=null]\n * @property {string | null} [fullSelector=null]\n * @property {boolean} [followCursor=true]\n * @property {string | null} [cursor=null]\n * @property {() => boolean} [enable=() => false]\n * @property {(HTMLElement) => boolean} [preventDrag=(el) => false]\n * @property {Position} [pointer={ x: 0, y: 0 }]\n * @property {EdgeScrollingOptions} [edgeScrolling]\n * @property {number} [delay]\n * @property {number} [tolerance]\n * @property {DraggableHookCurrentContext} current\n *\n * @typedef DraggableHookCurrentContext\n * @property {HTMLElement} [current.container]\n * @property {DOMRect} [current.containerRect]\n * @property {HTMLElement} [current.element]\n * @property {DOMRect} [current.elementRect]\n * @property {HTMLElement | null} [current.scrollParentX]\n * @property {DOMRect | null} [current.scrollParentXRect]\n * @property {HTMLElement | null} [current.scrollParentY]\n * @property {DOMRect | null} [current.scrollParentYRect]\n * @property {\"left\"|\"right\"|\"top\"|\"bottom\"|null} [scrollingEdge]\n * @property {number} [timeout]\n * @property {Position} [initialPosition]\n * @property {Position} [offset={ x: 0, y: 0 }]\n *\n * @typedef EdgeScrollingOptions\n * @property {boolean} [enabled=true]\n * @property {number} [speed=10]\n * @property {number} [threshold=20]\n * @property {\"horizontal\"|\"vertical\"} [direction]\n *\n * @typedef Position\n * @property {number} x\n * @property {number} y\n *\n * @typedef {DOMHelpers & {\n *  ctx: DraggableHookContext,\n *  addCleanup(cleanupFn: () => any): void,\n *  addEffectCleanup(cleanupFn: () => any): void,\n *  callHandler(handlerName: string, arg: Record<any, any>): void,\n * }} DraggableBuildHandlerParams\n *\n * @typedef {DOMHelpers & Position & { element: HTMLElement }} DraggableHandlerParams\n */\n\nconst DRAGGABLE_CLASS = \"o_draggable\";\nexport const DRAGGED_CLASS = \"o_dragged\";\n\nconst DEFAULT_ACCEPTED_PARAMS = {\n    enable: [Boolean, Function],\n    preventDrag: [Function],\n    ref: [Object],\n    elements: [String],\n    handle: [String, Function],\n    ignore: [String, Function],\n    cursor: [String],\n    edgeScrolling: [Object, Function],\n    delay: [Number],\n    tolerance: [Number],\n    touchDelay: [Number],\n    iframeWindow: [Object, Function],\n};\nconst DEFAULT_DEFAULT_PARAMS = {\n    elements: `.${DRAGGABLE_CLASS}`,\n    enable: true,\n    preventDrag: () => false,\n    edgeScrolling: {\n        speed: 10,\n        threshold: 30,\n    },\n    delay: 0,\n    tolerance: 10,\n    touchDelay: 300,\n};\nconst LEFT_CLICK = 0;\nconst MANDATORY_PARAMS = [\"ref\"];\nconst WHITE_LISTED_KEYS = [\"Alt\", \"Control\", \"Meta\", \"Shift\"];\n\n/**\n * Cache containing the elements in which an attribute has been modified by a hook.\n * It is global since multiple draggable hooks can interact with the same elements.\n * @type {Record<string, Set<HTMLElement>>}\n */\nconst elCache = {};\n\n/**\n * Transforms a camelCased string to return its kebab-cased version.\n * Typically used to generate CSS properties from JS objects.\n *\n * @param {string} str\n * @returns {string}\n */\nfunction camelToKebab(str) {\n    return str.replace(/([a-z])([A-Z])/g, \"$1-$2\").toLowerCase();\n}\n\n/**\n * @template T\n * @param {T | () => T} valueOrFn\n * @returns {T}\n */\nfunction getReturnValue(valueOrFn) {\n    if (typeof valueOrFn === \"function\") {\n        return valueOrFn();\n    }\n    return valueOrFn;\n}\n\n/**\n * Returns the first scrollable parent of the given element (recursively), or null\n * if none is found. A 'scrollable' element is defined by 2 things:\n *\n * - for either in width or in height: the 'scroll' value is larger than the 'client'\n * value;\n *\n * - its computed 'overflow' property is set to either \"auto\" or \"scroll\"\n *\n * If both of these assertions are true, it means that the element can effectively\n * be scrolled on at least one axis.\n * @param {HTMLElement} el\n * @returns {(HTMLElement | null)[]}\n */\nfunction getScrollParents(el) {\n    return [closestScrollableX(el), closestScrollableY(el)];\n}\n\n/**\n * @param {() => any} [defaultCleanupFn]\n */\nfunction makeCleanupManager(defaultCleanupFn) {\n    /**\n     * Registers the given cleanup function to be called when cleaning up hooks.\n     * @param {() => any} [cleanupFn]\n     */\n    const add = (cleanupFn) => typeof cleanupFn === \"function\" && cleanups.push(cleanupFn);\n\n    /**\n     * Runs all cleanup functions while clearing the cleanups list.\n     */\n    const cleanup = () => {\n        while (cleanups.length) {\n            cleanups.pop()();\n        }\n        add(defaultCleanupFn);\n    };\n\n    const cleanups = [];\n\n    add(defaultCleanupFn);\n\n    return { add, cleanup };\n}\n\n/**\n * @param {CleanupManager} cleanup\n */\nfunction makeDOMHelpers(cleanup) {\n    /**\n     * @param {HTMLElement} el\n     * @param  {...string} classNames\n     */\n    const addClass = (el, ...classNames) => {\n        if (!el || !classNames.length) {\n            return;\n        }\n        cleanup.add(() => el.classList.remove(...classNames));\n        el.classList.add(...classNames);\n    };\n\n    /**\n     * Adds an event listener to be cleaned up after the next drag sequence\n     * has stopped.\n     * @param {EventTarget} el\n     * @param {string} event\n     * @param {(...args: any[]) => any} callback\n     * @param {AddEventListenerOptions & { noAddedStyle?: boolean }} [options]\n     */\n    const addListener = (el, event, callback, options = {}) => {\n        if (!el || !event || !callback) {\n            return;\n        }\n        const { noAddedStyle } = options;\n        delete options.noAddedStyle;\n        el.addEventListener(event, callback, options);\n        if (!noAddedStyle && /mouse|pointer|touch/.test(event)) {\n            // Restore pointer events on elements listening on mouse/pointer/touch events.\n            addStyle(el, { pointerEvents: \"auto\" });\n        }\n        cleanup.add(() => el.removeEventListener(event, callback, options));\n    };\n\n    /**\n     * Adds style to an element to be cleaned up after the next drag sequence has\n     * stopped.\n     * @param {HTMLElement} el\n     * @param {Record<string, string | number>} style\n     */\n    const addStyle = (el, style) => {\n        if (!el || !style || !Object.keys(style).length) {\n            return;\n        }\n        cleanup.add(saveAttribute(el, \"style\"));\n        for (const key in style) {\n            const [value, priority] = String(style[key]).split(/\\s*!\\s*/);\n            el.style.setProperty(camelToKebab(key), value, priority);\n        }\n    };\n\n    /**\n     * Returns the bounding rect of the given element. If the `adjust` option is set\n     * to true, the rect will be reduced by the padding of the element.\n     * @param {HTMLElement} el\n     * @param {Object} [options={}]\n     * @param {boolean} [options.adjust=false]\n     * @returns {DOMRect}\n     */\n    const getRect = (el, options = {}) => {\n        if (!el) {\n            return {};\n        }\n        const rect = el.getBoundingClientRect();\n        if (options.adjust) {\n            const style = getComputedStyle(el);\n            const [pl, pr, pt, pb] = [\n                \"padding-left\",\n                \"padding-right\",\n                \"padding-top\",\n                \"padding-bottom\",\n            ].map((prop) => pixelValueToNumber(style.getPropertyValue(prop)));\n\n            rect.x += pl;\n            rect.y += pt;\n            rect.width -= pl + pr;\n            rect.height -= pt + pb;\n        }\n        return rect;\n    };\n\n    /**\n     * @param {HTMLElement} el\n     * @param {string} attribute\n     */\n    const removeAttribute = (el, attribute) => {\n        if (!el || !attribute) {\n            return;\n        }\n        cleanup.add(saveAttribute(el, attribute));\n        el.removeAttribute(attribute);\n    };\n\n    /**\n     * @param {HTMLElement} el\n     * @param {...string} classNames\n     */\n    const removeClass = (el, ...classNames) => {\n        if (!el || !classNames.length) {\n            return;\n        }\n        cleanup.add(saveAttribute(el, \"class\"));\n        el.classList.remove(...classNames);\n    };\n\n    /**\n     * Adds style to an element to be cleaned up after the next drag sequence has\n     * stopped.\n     * @param {HTMLElement} el\n     * @param {...string} properties\n     */\n    const removeStyle = (el, ...properties) => {\n        if (!el || !properties.length) {\n            return;\n        }\n        cleanup.add(saveAttribute(el, \"style\"));\n        for (const key of properties) {\n            el.style.removeProperty(camelToKebab(key));\n        }\n    };\n\n    /**\n     * @param {HTMLElement} el\n     * @param {string} attribute\n     * @param {any} value\n     */\n    const setAttribute = (el, attribute, value) => {\n        if (!el || !attribute) {\n            return;\n        }\n        cleanup.add(saveAttribute(el, attribute));\n        el.setAttribute(attribute, String(value));\n    };\n\n    return {\n        addClass,\n        addListener,\n        addStyle,\n        getRect,\n        removeAttribute,\n        removeClass,\n        removeStyle,\n        setAttribute,\n    };\n}\n\n/**\n * Converts a CSS pixel value to a number, removing the 'px' part.\n * @param {string} val\n * @returns {number}\n */\nfunction pixelValueToNumber(val) {\n    return Number(val.endsWith(\"px\") ? val.slice(0, -2) : val);\n}\n\n/**\n * @param {Event} ev\n * @param {{ stop?: boolean }} params\n */\nfunction safePrevent(ev, { stop } = {}) {\n    if (ev.cancelable) {\n        ev.preventDefault();\n        if (stop) {\n            ev.stopPropagation();\n        }\n    }\n}\n\nfunction saveAttribute(el, attribute) {\n    const restoreAttribute = () => {\n        cache.delete(el);\n        if (hasAttribute) {\n            el.setAttribute(attribute, originalValue);\n        } else {\n            el.removeAttribute(attribute);\n        }\n    };\n\n    if (!(attribute in elCache)) {\n        elCache[attribute] = new Set();\n    }\n    const cache = elCache[attribute];\n\n    if (cache.has(el)) {\n        return;\n    }\n\n    cache.add(el);\n    const hasAttribute = el.hasAttribute(attribute);\n    const originalValue = el.getAttribute(attribute);\n\n    return restoreAttribute;\n}\n\n/**\n * @template T\n * @param {T | () => T} value\n * @returns {() => T}\n */\nfunction toFunction(value) {\n    return typeof value === \"function\" ? value : () => value;\n}\n\n/**\n * @param {DraggableBuilderParams} hookParams\n * @returns {(params: Record<keyof typeof DEFAULT_ACCEPTED_PARAMS, any>) => { dragging: boolean }}\n */\nexport function makeDraggableHook(hookParams) {\n    hookParams = getReturnValue(hookParams);\n\n    const hookName = hookParams.name || \"useAnonymousDraggable\";\n    const { setupHooks } = hookParams;\n    const allAcceptedParams = { ...DEFAULT_ACCEPTED_PARAMS, ...hookParams.acceptedParams };\n    const defaultParams = { ...DEFAULT_DEFAULT_PARAMS, ...hookParams.defaultParams };\n\n    /**\n     * Computes the current params and converts the params definition\n     * @param {SortableParams} params\n     * @returns {[string, string | boolean][]}\n     */\n    const computeParams = (params) => {\n        const computedParams = { enable: () => true };\n        for (const prop in allAcceptedParams) {\n            if (prop in params) {\n                if (prop === \"enable\") {\n                    computedParams[prop] = toFunction(params[prop]);\n                } else if (\n                    allAcceptedParams[prop].length === 1 &&\n                    allAcceptedParams[prop][0] === Function\n                ) {\n                    computedParams[prop] = params[prop];\n                } else {\n                    computedParams[prop] = getReturnValue(params[prop]);\n                }\n            }\n        }\n        return Object.entries(computedParams);\n    };\n\n    /**\n     * Basic error builder for the hook.\n     * @param {string} reason\n     * @returns {Error}\n     */\n    const makeError = (reason) => new Error(`Error in hook ${hookName}: ${reason}.`);\n    let preventClick = false;\n\n    return {\n        [hookName](params) {\n            /**\n             * Executes a handler from the `hookParams`.\n             * @param {string} hookHandlerName\n             * @param {Record<any, any>} arg\n             */\n            const callBuildHandler = (hookHandlerName, arg) => {\n                if (typeof hookParams[hookHandlerName] !== \"function\") {\n                    return;\n                }\n                const returnValue = hookParams[hookHandlerName]({ ctx, ...helpers, ...arg });\n                if (returnValue) {\n                    callHandler(hookHandlerName, returnValue);\n                }\n            };\n\n            /**\n             * Safely executes a handler from the `params`, so that the drag sequence can\n             * be interrupted if an error occurs.\n             * @param {string} handlerName\n             * @param {Record<any, any>} arg\n             */\n            const callHandler = (handlerName, arg) => {\n                if (typeof params[handlerName] !== \"function\") {\n                    return;\n                }\n                try {\n                    params[handlerName]({ ...dom, ...ctx.pointer, ...arg });\n                } catch (err) {\n                    dragEnd(null, true);\n                    throw err;\n                }\n            };\n\n            /**\n             * Returns whether the user has moved from at least the number of pixels\n             * that are tolerated from the initial pointer position.\n             */\n            const canStartDrag = () => {\n                const {\n                    pointer,\n                    current: { initialPosition },\n                } = ctx;\n                return (\n                    !ctx.tolerance ||\n                    Math.hypot(pointer.x - initialPosition.x, pointer.y - initialPosition.y) >=\n                        ctx.tolerance\n                );\n            };\n\n            /**\n             * Main entry function to start a drag sequence.\n             */\n            const dragStart = () => {\n                state.dragging = true;\n                state.willDrag = false;\n\n                // Compute scrollable parent\n                const isDocumentScrollingElement = ctx.current.container\n                    === ctx.current.container.ownerDocument.scrollingElement;\n                // If the container is the \"ownerDocument.scrollingElement\",\n                // there is no need to get the scroll parent as it is the\n                // scrollable element itself.\n                // TODO: investigate if \"getScrollParents\" should not consider\n                // the \"ownerDocument.scrollingElement\" directly.\n                [ctx.current.scrollParentX, ctx.current.scrollParentY] =\n                    isDocumentScrollingElement\n                    ? [ctx.current.container, ctx.current.container]\n                    : getScrollParents(ctx.current.container);\n\n                updateRects();\n                const { x, y, width, height } = ctx.current.elementRect;\n\n                // Adjusts the offset\n                ctx.current.offset = {\n                    x: ctx.current.initialPosition.x - x,\n                    y: ctx.current.initialPosition.y - y,\n                };\n\n                if (ctx.followCursor) {\n                    dom.addStyle(ctx.current.element, {\n                        width: `${width}px`,\n                        height: `${height}px`,\n                        position: \"fixed !important\",\n                    });\n\n                    // First adjustment\n                    updateElementPosition();\n                }\n\n                dom.addClass(document.body, \"pe-none\", \"user-select-none\");\n                if (params.iframeWindow) {\n                    for (const iframe of document.getElementsByTagName(\"iframe\")) {\n                        if (iframe.contentWindow === params.iframeWindow) {\n                            dom.addClass(iframe, \"pe-none\", \"user-select-none\");\n                        }\n                    }\n                }\n                // FIXME: adding pe-none and cursor on the same element makes\n                // no sense as pe-none prevents the cursor to be displayed.\n                if (ctx.cursor) {\n                    dom.addStyle(document.body, { cursor: ctx.cursor });\n                }\n\n                if (\n                    (ctx.current.scrollParentX || ctx.current.scrollParentY) &&\n                    ctx.edgeScrolling.enabled\n                ) {\n                    const cleanupFn = setRecurringAnimationFrame(handleEdgeScrolling);\n                    cleanup.add(cleanupFn);\n                }\n\n                dom.addClass(ctx.current.element, DRAGGED_CLASS);\n\n                callBuildHandler(\"onDragStart\");\n            };\n\n            /**\n             * Main exit function to stop a drag sequence. Note that it can be called\n             * even if a drag sequence did not start yet to perform a cleanup of all\n             * current context variables.\n             * @param {HTMLElement | null} target\n             * @param {boolean} [inErrorState] can be set to true when an error\n             *  occurred to avoid falling into an infinite loop if the error\n             *  originated from one of the handlers.\n             */\n            const dragEnd = (target, inErrorState) => {\n                if (state.dragging) {\n                    preventClick = true;\n                    if (!inErrorState) {\n                        if (target) {\n                            callBuildHandler(\"onDrop\", { target });\n                        }\n                        callBuildHandler(\"onDragEnd\");\n                    }\n                }\n\n                cleanup.cleanup();\n            };\n\n            /**\n             * Applies scroll to the container if the current element is near\n             * the edge of the container.\n             */\n            const handleEdgeScrolling = (deltaTime) => {\n                updateRects();\n                const { x: pointerX, y: pointerY } = ctx.pointer;\n                const xRect = ctx.current.scrollParentXRect;\n                const yRect = ctx.current.scrollParentYRect;\n\n                // \"getBoundingClientRect()\"\" (used in \"getRect()\") gives the\n                // distance from the element's top to the viewport, excluding\n                // scroll position. Only the \"document.scrollingElement\" element\n                // (\"<html>\") accounts for scrollTop.\n                const scrollParentYEl = ctx.current.scrollParentY;\n                if (scrollParentYEl === ctx.current.container.ownerDocument.scrollingElement) {\n                    yRect.y += scrollParentYEl.scrollTop;\n                }\n\n                const { direction, speed, threshold } = ctx.edgeScrolling;\n                const correctedSpeed = (speed / 16) * deltaTime;\n\n                const diff = {};\n                ctx.current.scrollingEdge = null;\n                if (xRect) {\n                    const maxWidth = xRect.x + xRect.width;\n                    if (pointerX - xRect.x < threshold) {\n                        diff.x = [pointerX - xRect.x, -1];\n                        ctx.current.scrollingEdge = \"left\";\n                    } else if (maxWidth - pointerX < threshold) {\n                        diff.x = [maxWidth - pointerX, 1];\n                        ctx.current.scrollingEdge = \"right\";\n                    }\n                }\n                if (yRect) {\n                    const maxHeight = yRect.y + yRect.height;\n                    if (pointerY - yRect.y < threshold) {\n                        diff.y = [pointerY - yRect.y, -1];\n                        ctx.current.scrollingEdge = \"top\";\n                    } else if (maxHeight - pointerY < threshold) {\n                        diff.y = [maxHeight - pointerY, 1];\n                        ctx.current.scrollingEdge = \"bottom\";\n                    }\n                }\n\n                const diffToScroll = ([delta, sign]) =>\n                    (1 - Math.max(delta, 0) / threshold) * correctedSpeed * sign;\n                if ((!direction || direction === \"vertical\") && diff.y) {\n                    ctx.current.scrollParentY.scrollBy({ top: diffToScroll(diff.y) });\n                }\n                if ((!direction || direction === \"horizontal\") && diff.x) {\n                    ctx.current.scrollParentX.scrollBy({ left: diffToScroll(diff.x) });\n                }\n                callBuildHandler(\"onDrag\");\n            };\n\n            /**\n             * Global (= ref) \"click\" event handler.\n             * Used to prevent click events after dragEnd\n             * @param {PointerEvent} ev\n             */\n            const onClick = (ev) => {\n                if (preventClick) {\n                    safePrevent(ev, { stop: true });\n                }\n            };\n\n            /**\n             * Window \"keydown\" event handler.\n             * @param {KeyboardEvent} ev\n             */\n            const onKeyDown = (ev) => {\n                if (!state.dragging || !ctx.enable()) {\n                    return;\n                }\n                if (!WHITE_LISTED_KEYS.includes(ev.key)) {\n                    safePrevent(ev, { stop: true });\n\n                    // Cancels drag sequences on every non-whitelisted key down event.\n                    dragEnd(null);\n                }\n            };\n\n            /**\n             * Global (= ref) \"pointercancel\" event handler.\n             */\n            const onPointerCancel = () => {\n                dragEnd(null);\n            };\n\n            /**\n             * Global (= ref) \"pointerdown\" event handler.\n             * @param {PointerEvent} ev\n             */\n            const onPointerDown = (ev) => {\n                preventClick = false;\n                updatePointerPosition(ev);\n\n                const initiationDelay = ev.pointerType === \"touch\" ? ctx.touchDelay : ctx.delay;\n\n                // A drag sequence can still be in progress if the pointerup occurred\n                // outside of the window.\n                dragEnd(null);\n\n                const fullSelectorEl = ev.target.closest(ctx.fullSelector);\n                if (\n                    ev.button !== LEFT_CLICK ||\n                    !ctx.enable() ||\n                    !fullSelectorEl ||\n                    (ctx.ignoreSelector && ev.target.closest(ctx.ignoreSelector)) ||\n                    ctx.preventDrag(fullSelectorEl)\n                ) {\n                    return;\n                }\n\n                // In FireFox: elements with `overflow: hidden` will prevent mouseenter and mouseleave\n                // events from firing on elements underneath them. This is the case when dragging a card\n                // by the heading. In such cases, we can prevent the default\n                // action on the pointerdown event to allow pointer events to fire properly.\n                // https://bugzilla.mozilla.org/show_bug.cgi?id=1352061\n                // https://bugzilla.mozilla.org/show_bug.cgi?id=339293\n                safePrevent(ev);\n                let activeElement = document.activeElement;\n                while (activeElement?.nodeName === \"IFRAME\") {\n                    activeElement = activeElement.contentDocument.activeElement;\n                }\n                if (activeElement && !activeElement.contains(ev.target)) {\n                    activeElement.blur();\n                }\n\n                const { currentTarget, pointerId, target } = ev;\n                ctx.current.initialPosition = { ...ctx.pointer };\n\n                if (target.hasPointerCapture(pointerId)) {\n                    target.releasePointerCapture(pointerId);\n                }\n\n                if (initiationDelay) {\n                    if (hasTouch()) {\n                        if (ev.pointerType === \"touch\") {\n                            dom.addClass(target.closest(ctx.elementSelector), \"o_touch_bounce\");\n                        }\n                        if (isBrowserFirefox()) {\n                            // On Firefox mobile, long-touch events trigger an unpreventable\n                            // context menu to appear. To prevent this, all linkes are removed\n                            // from the dragged elements during the drag sequence.\n                            const links = [...currentTarget.querySelectorAll(\"[href]\")];\n                            if (currentTarget.hasAttribute(\"href\")) {\n                                links.unshift(currentTarget);\n                            }\n                            for (const link of links) {\n                                dom.removeAttribute(link, \"href\");\n                            }\n                        }\n                        if (isIOS()) {\n                            // On Safari mobile, any image can be dragged regardless\n                            // of the 'user-select' property.\n                            for (const image of currentTarget.getElementsByTagName(\"img\")) {\n                                dom.setAttribute(image, \"draggable\", false);\n                            }\n                        }\n                    }\n\n                    ctx.current.timeout = browser.setTimeout(() => {\n                        ctx.current.initialPosition = { ...ctx.pointer };\n\n                        willStartDrag(target);\n\n                        const { x: px, y: py } = ctx.pointer;\n                        const { x, y, width, height } = dom.getRect(ctx.current.element);\n                        if (px < x || x + width < px || py < y || y + height < py) {\n                            // Pointer left the target\n                            // Note that the timeout is cleared in dragEnd\n                            dragEnd(null);\n                        }\n                    }, initiationDelay);\n                    cleanup.add(() => browser.clearTimeout(ctx.current.timeout));\n                } else {\n                    willStartDrag(target);\n                }\n            };\n\n            /**\n             * Window \"pointermove\" event handler.\n             * @param {PointerEvent} ev\n             */\n            const onPointerMove = (ev) => {\n                updatePointerPosition(ev);\n\n                if (!ctx.current.element || !ctx.enable()) {\n                    return;\n                }\n\n                safePrevent(ev);\n\n                if (!state.dragging) {\n                    if (!canStartDrag()) {\n                        return;\n                    }\n                    dragStart();\n                }\n\n                if (ctx.followCursor) {\n                    updateElementPosition();\n                }\n\n                callBuildHandler(\"onDrag\");\n            };\n\n            /**\n             * Window \"pointerup\" event handler.\n             * @param {PointerEvent} ev\n             */\n            const onPointerUp = (ev) => {\n                updatePointerPosition(ev);\n                dragEnd(ev.target);\n            };\n\n            /**\n             * Updates the position of the current dragged element according to\n             * the current pointer position.\n             */\n            const updateElementPosition = () => {\n                const { containerRect, element, elementRect, offset } = ctx.current;\n                const { width: ew, height: eh } = elementRect;\n                const { x: cx, y: cy, width: cw, height: ch } = containerRect;\n\n                // Updates the position of the dragged element.\n                dom.addStyle(element, {\n                    left: `${clamp(ctx.pointer.x - offset.x, cx, cx + cw - ew)}px`,\n                    top: `${clamp(ctx.pointer.y - offset.y, cy, cy + ch - eh)}px`,\n                });\n            };\n\n            /**\n             * Updates the current pointer position from a given event.\n             * @param {PointerEvent} ev\n             */\n            const updatePointerPosition = (ev) => {\n                ctx.pointer.x = ev.clientX;\n                ctx.pointer.y = ev.clientY;\n            };\n\n            const updateRects = () => {\n                const { current } = ctx;\n                const { container, element, scrollParentX, scrollParentY } = current;\n                // Container rect\n                current.containerRect = dom.getRect(container, { adjust: true });\n                // If the scrolling element is within an iframe and the draggable\n                // element is outside this iframe, the offsets must be computed taking\n                // into account the iframe.\n                let iframeOffsetX = 0;\n                let iframeOffsetY = 0;\n                const iframeEl = container.ownerDocument.defaultView.frameElement;\n                if (iframeEl && !iframeEl.contentDocument.contains(element)) {\n                    const { x, y } = dom.getRect(iframeEl);\n                    iframeOffsetX = x;\n                    iframeOffsetY = y;\n                    current.containerRect.x += iframeOffsetX;\n                    current.containerRect.y += iframeOffsetY;\n                }\n                // Adjust container rect according to its overflowing size\n                current.containerRect.width = container.scrollWidth;\n                current.containerRect.height = container.scrollHeight;\n                // ScrollParent rect\n                current.scrollParentXRect = null;\n                current.scrollParentYRect = null;\n                if (ctx.edgeScrolling.enabled) {\n                    // Adjust container rect according to scrollParents\n                    if (scrollParentX) {\n                        current.scrollParentXRect = dom.getRect(scrollParentX, { adjust: true });\n                        current.scrollParentXRect.x += iframeOffsetX;\n                        current.scrollParentXRect.y += iframeOffsetY;\n                        const right = Math.min(\n                            current.containerRect.left + container.scrollWidth,\n                            current.scrollParentXRect.right\n                        );\n                        current.containerRect.x = Math.max(\n                            current.containerRect.x,\n                            current.scrollParentXRect.x\n                        );\n                        current.containerRect.width = right - current.containerRect.x;\n                    }\n                    if (scrollParentY) {\n                        current.scrollParentYRect = dom.getRect(scrollParentY, { adjust: true });\n                        current.scrollParentYRect.x += iframeOffsetX;\n                        current.scrollParentYRect.y += iframeOffsetY;\n                        const bottom = Math.min(\n                            current.containerRect.top + container.scrollHeight,\n                            current.scrollParentYRect.bottom\n                        );\n                        current.containerRect.y = Math.max(\n                            current.containerRect.y,\n                            current.scrollParentYRect.y\n                        );\n                        current.containerRect.height = bottom - current.containerRect.y;\n                    }\n                }\n\n                // Element rect\n                ctx.current.elementRect = dom.getRect(element);\n            };\n\n            /**\n             * @param {Element} target\n             */\n            const willStartDrag = (target) => {\n                ctx.current.element = target.closest(ctx.elementSelector);\n                ctx.current.container = ctx.ref.el;\n\n                cleanup.add(() => (ctx.current = {}));\n                state.willDrag = true;\n\n                callBuildHandler(\"onWillStartDrag\");\n\n                if (hasTouch()) {\n                    // Prevents panning/zooming after a long press\n                    dom.addListener(window, \"touchmove\", safePrevent, {\n                        passive: false,\n                        noAddedStyle: true,\n                    });\n                    if (params.iframeWindow) {\n                        dom.addListener(params.iframeWindow, \"touchmove\", safePrevent, {\n                            passive: false,\n                            noAddedStyle: true,\n                        });\n                    }\n                }\n            };\n\n            // Initialize helpers\n            const cleanup = makeCleanupManager(() => (state.dragging = false));\n            const effectCleanup = makeCleanupManager();\n            const dom = makeDOMHelpers(cleanup);\n\n            const helpers = {\n                ...dom,\n                addCleanup: cleanup.add,\n                addEffectCleanup: effectCleanup.add,\n                callHandler,\n            };\n\n            // Component infos\n            const state = setupHooks.wrapState({ dragging: false });\n\n            // Basic error handling asserting that the parameters are valid.\n            for (const prop in allAcceptedParams) {\n                const type = typeof params[prop];\n                const acceptedTypes = allAcceptedParams[prop].map((t) => t.name.toLowerCase());\n                if (params[prop]) {\n                    if (!acceptedTypes.includes(type)) {\n                        throw makeError(\n                            `invalid type for property \"${prop}\" in parameters: expected { ${acceptedTypes.join(\n                                \", \"\n                            )} } and got ${type}`\n                        );\n                    }\n                } else if (MANDATORY_PARAMS.includes(prop) && !defaultParams[prop]) {\n                    throw makeError(`missing required property \"${prop}\" in parameters`);\n                }\n            }\n\n            /** @type {DraggableHookContext} */\n            const ctx = {\n                enable: () => false,\n                preventDrag: () => false,\n                ref: params.ref,\n                ignoreSelector: null,\n                fullSelector: null,\n                followCursor: true,\n                cursor: null,\n                pointer: { x: 0, y: 0 },\n                edgeScrolling: { enabled: true },\n                get dragging() {\n                    return state.dragging;\n                },\n                get willDrag() {\n                    return state.willDrag;\n                },\n                // Current context\n                current: {},\n            };\n\n            // Effect depending on the params to update them.\n            setupHooks.setup(\n                (...deps) => {\n                    const params = Object.fromEntries(deps);\n                    const actualParams = { ...defaultParams, ...omit(params, \"edgeScrolling\") };\n                    if (params.edgeScrolling) {\n                        actualParams.edgeScrolling = {\n                            ...actualParams.edgeScrolling,\n                            ...params.edgeScrolling,\n                        };\n                    }\n\n                    if (!ctx.ref.el) {\n                        return;\n                    }\n\n                    // Enable getter\n                    ctx.enable = actualParams.enable;\n\n                    // Dragging constraint\n                    if (actualParams.preventDrag) {\n                        ctx.preventDrag = actualParams.preventDrag;\n                    }\n\n                    // Selectors\n                    ctx.elementSelector = actualParams.elements;\n                    if (!ctx.elementSelector) {\n                        throw makeError(\n                            `no value found by \"elements\" selector: ${ctx.elementSelector}`\n                        );\n                    }\n                    const allSelectors = [ctx.elementSelector];\n                    ctx.cursor = actualParams.cursor || null;\n                    if (actualParams.handle) {\n                        allSelectors.push(actualParams.handle);\n                    }\n                    if (actualParams.ignore) {\n                        ctx.ignoreSelector = actualParams.ignore;\n                    }\n                    ctx.fullSelector = allSelectors.join(\" \");\n\n                    // Edge scrolling\n                    Object.assign(ctx.edgeScrolling, actualParams.edgeScrolling);\n\n                    // Delay & tolerance\n                    ctx.delay = actualParams.delay;\n                    ctx.touchDelay = actualParams.delay || actualParams.touchDelay;\n                    ctx.tolerance = actualParams.tolerance;\n\n                    callBuildHandler(\"onComputeParams\", { params: actualParams });\n\n                    // Calls effect cleanup functions when preparing to re-render.\n                    return effectCleanup.cleanup;\n                },\n                () => computeParams(params)\n            );\n            // Firefox currently (119.0.1) does not handle our pointer events\n            // nicely when they happen from within the iframe. To work around\n            // this, we use mouse events instead of pointer events.\n            const useMouseEvents = isBrowserFirefox() && !hasTouch() && params.iframeWindow;\n            // Effect depending on the `ref.el` to add triggering pointer events listener.\n            setupHooks.setup(\n                (el) => {\n                    if (el) {\n                        const { add, cleanup } = makeCleanupManager();\n                        const { addListener } = makeDOMHelpers({ add });\n                        const event = useMouseEvents ? \"mousedown\" : \"pointerdown\";\n                        addListener(el, event, onPointerDown, { noAddedStyle: true });\n                        addListener(el, \"click\", onClick);\n                        if (hasTouch()) {\n                            addListener(el, \"contextmenu\", safePrevent);\n                            // Adds a non-passive listener on touchstart: this allows\n                            // the subsequent \"touchmove\" events to be cancelable\n                            // and thus prevent parasitic \"touchcancel\" events to\n                            // be fired. Note that we DO NOT want to prevent touchstart\n                            // events since they're responsible of the native swipe\n                            // scrolling.\n                            addListener(el, \"touchstart\", () => {}, {\n                                passive: false,\n                                noAddedStyle: true,\n                            });\n                        }\n                        return cleanup;\n                    }\n                },\n                () => [ctx.ref.el]\n            );\n            const addWindowListener = (type, listener, options) => {\n                if (params.iframeWindow) {\n                    setupHooks.addListener(params.iframeWindow, type, listener, options);\n                }\n                setupHooks.addListener(window, type, listener, options);\n            };\n            // Other global event listeners.\n            const throttledOnPointerMove = setupHooks.throttle(onPointerMove);\n            addWindowListener(\n                useMouseEvents ? \"mousemove\" : \"pointermove\",\n                throttledOnPointerMove,\n                { passive: false }\n            );\n            addWindowListener(useMouseEvents ? \"mouseup\" : \"pointerup\", onPointerUp);\n            addWindowListener(\"pointercancel\", onPointerCancel);\n            addWindowListener(\"keydown\", onKeyDown, { capture: true });\n            setupHooks.teardown(() => dragEnd(null));\n\n            return state;\n        },\n    }[hookName];\n}\n", "import { onWillUnmount, reactive, useEffect, useExternalListener } from \"@odoo/owl\";\nimport { useThrottleForAnimation } from \"./timing\";\nimport { makeDraggableHook as nativeMakeDraggableHook } from \"./draggable_hook_builder\";\n\n/**\n * Set of default `makeDraggableHook` setup hooks that makes use of Owl lifecycle\n * and reactivity hooks to properly set up, update and tear down the elements and\n * listeners added by the draggable hook builder.\n *\n * @see {nativeMakeDraggableHook}\n * @type {typeof nativeMakeDraggableHook}\n */\nexport function makeDraggableHook(params) {\n    return nativeMakeDraggableHook({\n        ...params,\n        setupHooks: {\n            addListener: useExternalListener,\n            setup: useEffect,\n            teardown: onWillUnmount,\n            throttle: useThrottleForAnimation,\n            wrapState: reactive,\n        },\n    });\n}\n", "import { humanNumber } from \"@web/core/utils/numbers\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { session } from \"@web/session\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport const DEFAULT_MAX_FILE_SIZE = 128 * 1024 * 1024;\n\n/**\n * @param {Services[\"notification\"]} notificationService\n * @param {File} file\n * @param {Number} maxUploadSize\n * @returns {boolean}\n */\nexport function checkFileSize(fileSize, notificationService) {\n    const maxUploadSize = session.max_file_upload_size || DEFAULT_MAX_FILE_SIZE;\n    if (fileSize > maxUploadSize) {\n        notificationService.add(\n            _t(\n                \"The selected file (%(size)sB) is larger than the maximum allowed file size (%(maxSize)sB).\",\n                { size: humanNumber(fileSize), maxSize: humanNumber(maxUploadSize) }\n            ),\n            {\n                type: \"danger\",\n            }\n        );\n        return false;\n    }\n    return true;\n}\n\n/**\n * Hook to upload a file to the server.\n * @returns {function}\n */\nexport function useFileUploader() {\n    const http = useService(\"http\");\n    const notification = useService(\"notification\");\n    /**\n     * @param {string} route\n     * @param {Object} params\n     */\n    return async (route, params) => {\n        if ((params.ufile && params.ufile.length) || params.file) {\n            const fileSize = (params.ufile && params.ufile[0].size) || params.file.size;\n            if (!checkFileSize(fileSize, notification)) {\n                return null;\n            }\n        }\n        const fileData = await http.post(route, params, \"text\");\n        const parsedFileData = JSON.parse(fileData);\n        if (parsedFileData.error) {\n            throw new Error(parsedFileData.error);\n        }\n        return parsedFileData;\n    };\n}\n", "/**\n * Creates a version of the function that's memoized on the value of its first\n * argument, if any.\n *\n * @template T, U\n * @param {(arg: T) => U} func the function to memoize\n * @returns {(arg: T) => U} a memoized version of the original function\n */\nexport function memoize(func) {\n    const cache = new Map();\n    const funcName = func.name ? func.name + \" (memoized)\" : \"memoized\";\n    return {\n        [funcName](...args) {\n            if (!cache.has(args[0])) {\n                cache.set(args[0], func(...args));\n            }\n            return cache.get(...args);\n        },\n    }[funcName];\n}\n\n/**\n * Generate a unique integer id (unique within the entire client session).\n * Useful for temporary DOM ids.\n *\n * @param {string} prefix\n * @returns {string}\n */\nexport function uniqueId(prefix = \"\") {\n    return `${prefix}${++uniqueId.nextId}`;\n}\n// set nextId on the function itself to be able to patch then\nuniqueId.nextId = 0;\n", "import { hasTouch, isMobileOS } from \"@web/core/browser/feature_detection\";\n\nimport { status, useComponent, useEffect, useRef, onWillUnmount } from \"@odoo/owl\";\n\n/**\n * This file contains various custom hooks.\n * Their inner working is rather simple:\n * Each custom hook simply hooks itself to any number of owl lifecycle hooks.\n * You can then use them just like an owl hook in any Component\n * e.g.:\n * import { useBus } from \"@web/core/utils/hooks\";\n * ...\n * setup() {\n *    ...\n *    useBus(someBus, someEvent, callback)\n *    ...\n * }\n */\n\n/**\n * @typedef {{ readonly el: HTMLElement | null; }} Ref\n */\n\n// -----------------------------------------------------------------------------\n// useAutofocus\n// -----------------------------------------------------------------------------\n\n/**\n * Focus an element referenced by a t-ref=\"autofocus\" in the active component\n * as soon as it appears in the DOM and if it was not displayed before.\n * If it is an input/textarea, set the selection at the end.\n * @param {Object} [params]\n * @param {string} [params.refName] override the ref name \"autofocus\"\n * @param {boolean} [params.selectAll] if true, will select the entire text value.\n * @param {boolean} [params.mobile] if true, will force autofocus on touch devices.\n * @returns {Ref} the element reference\n */\nexport function useAutofocus({ refName, selectAll, mobile } = {}) {\n    const ref = useRef(refName || \"autofocus\");\n    const uiService = useService(\"ui\");\n\n    // Prevent autofocus on touch devices to avoid the virtual keyboard from popping up unexpectedly\n    if (!mobile && hasTouch()) {\n        return ref;\n    }\n    // LEGACY\n    if (!mobile && isMobileOS()) {\n        return ref;\n    }\n    // LEGACY\n    useEffect(\n        (el) => {\n            if (el && (!uiService.activeElement || uiService.activeElement.contains(el))) {\n                el.focus();\n                if ([\"INPUT\", \"TEXTAREA\"].includes(el.tagName) && el.type !== \"number\") {\n                    el.selectionEnd = el.value.length;\n                    el.selectionStart = selectAll ? 0 : el.value.length;\n                }\n            }\n        },\n        () => [ref.el]\n    );\n    return ref;\n}\n\n// -----------------------------------------------------------------------------\n// useBus\n// -----------------------------------------------------------------------------\n\n/**\n * Ensures a bus event listener is attached and cleared the proper way.\n *\n * @param {import(\"@odoo/owl\").EventBus} bus\n * @param {string} eventName\n * @param {EventListener} callback\n */\nexport function useBus(bus, eventName, callback) {\n    const component = useComponent();\n    useEffect(\n        () => {\n            const listener = callback.bind(component);\n            bus.addEventListener(eventName, listener);\n            return () => bus.removeEventListener(eventName, listener);\n        },\n        () => []\n    );\n}\n\n// In an object so that it can be patched in tests (prevent error on blocking RPCs after tests)\nexport const useServiceProtectMethodHandling = {\n    fn() {\n        return this.original();\n    },\n    mocked() {\n        // Keep them unresolved so that no crash in test due to triggered RPCs by services\n        return new Promise(() => {});\n    },\n    original() {\n        return Promise.reject(new Error(\"Component is destroyed\"));\n    },\n};\n\n// -----------------------------------------------------------------------------\n// useService\n// -----------------------------------------------------------------------------\nfunction _protectMethod(component, fn) {\n    return function (...args) {\n        if (status(component) === \"destroyed\") {\n            return useServiceProtectMethodHandling.fn();\n        }\n\n        const prom = Promise.resolve(fn.call(this, ...args));\n        const protectedProm = prom.then((result) =>\n            status(component) === \"destroyed\" ? new Promise(() => {}) : result\n        );\n        return Object.assign(protectedProm, {\n            abort: prom.abort,\n            cancel: prom.cancel,\n        });\n    };\n}\n\nexport const SERVICES_METADATA = {};\n\n/**\n * Import a service into a component\n *\n * @template {keyof import(\"services\").ServiceFactories} K\n * @param {K} serviceName\n * @returns {import(\"services\").ServiceFactories[K]}\n */\nexport function useService(serviceName) {\n    const component = useComponent();\n    const { services } = component.env;\n    if (!(serviceName in services)) {\n        throw new Error(`Service ${serviceName} is not available`);\n    }\n    const service = services[serviceName];\n    if (serviceName in SERVICES_METADATA) {\n        if (service instanceof Function) {\n            return _protectMethod(component, service);\n        } else {\n            const methods = SERVICES_METADATA[serviceName];\n            const result = Object.create(service);\n            for (const method of methods) {\n                result[method] = _protectMethod(component, service[method]);\n            }\n            return result;\n        }\n    }\n    return service;\n}\n\n// -----------------------------------------------------------------------------\n// useSpellCheck\n// -----------------------------------------------------------------------------\n\n/**\n * To avoid elements to keep their spellcheck appearance when they are no\n * longer in focus. We only add this attribute when needed. To disable this\n * behavior, use the spellcheck attribute on the element.\n */\nexport function useSpellCheck({ refName } = {}) {\n    const elements = [];\n    const ref = useRef(refName || \"spellcheck\");\n    function toggleSpellcheck(ev) {\n        ev.target.spellcheck = document.activeElement === ev.target;\n    }\n    useEffect(\n        (el) => {\n            if (el) {\n                const inputs =\n                    [\"INPUT\", \"TEXTAREA\"].includes(el.nodeName) || el.contenteditable\n                        ? [el]\n                        : el.querySelectorAll(\"input, textarea, [contenteditable=true]\");\n                inputs.forEach((input) => {\n                    if (input.spellcheck !== false) {\n                        elements.push(input);\n                        input.addEventListener(\"focus\", toggleSpellcheck);\n                        input.addEventListener(\"blur\", toggleSpellcheck);\n                    }\n                });\n            }\n            return () => {\n                elements.forEach((input) => {\n                    input.removeEventListener(\"focus\", toggleSpellcheck);\n                    input.removeEventListener(\"blur\", toggleSpellcheck);\n                });\n            };\n        },\n        () => [ref.el]\n    );\n}\n\n/**\n * @typedef {Function} ForwardRef\n * @property {HTMLElement | undefined} el\n */\n\n/**\n * Use a ref that was forwarded by a child @see useForwardRefToParent\n *\n * @returns {ForwardRef} a ref that can be called to set its value to that of a\n *  child ref, but can otherwise be used as a normal ref object\n */\nexport function useChildRef() {\n    let defined = false;\n    let value;\n    return function ref(v) {\n        value = v;\n        if (defined) {\n            return;\n        }\n        Object.defineProperty(ref, \"el\", {\n            get() {\n                return value.el;\n            },\n        });\n        defined = true;\n    };\n}\n/**\n * Forwards the given refName to the parent by calling the corresponding\n * ForwardRef received as prop. @see useChildRef\n *\n * @param {string} refName name of the ref to forward\n * @returns {Ref} the same ref that is forwarded to the\n *  parent\n */\nexport function useForwardRefToParent(refName) {\n    const component = useComponent();\n    const ref = useRef(refName);\n    if (component.props[refName]) {\n        component.props[refName](ref);\n    }\n    return ref;\n}\n/**\n * Use the dialog service while also automatically closing the dialogs opened\n * by the current component when it is unmounted.\n *\n * @returns {import(\"@web/core/dialog/dialog_service\").DialogServiceInterface}\n */\nexport function useOwnedDialogs() {\n    const dialogService = useService(\"dialog\");\n    const cbs = [];\n    onWillUnmount(() => {\n        cbs.forEach((cb) => cb());\n    });\n    const addDialog = (...args) => {\n        const close = dialogService.add(...args);\n        cbs.push(close);\n        return close;\n    };\n    return addDialog;\n}\n/**\n * Manages an event listener on a ref. Useful for hooks that want to manage\n * event listeners, especially more than one. Prefer using t-on directly in\n * components. If your hook only needs a single event listener, consider simply\n * returning it from the hook and letting the user attach it with t-on.\n *\n * @param {Ref} ref\n * @param {Parameters<typeof EventTarget.prototype.addEventListener>} listener\n */\nexport function useRefListener(ref, ...listener) {\n    useEffect(\n        (el) => {\n            el?.addEventListener(...listener);\n            return () => el?.removeEventListener(...listener);\n        },\n        () => [ref.el]\n    );\n}\n", "const eventHandledWeakMap = new WeakMap();\n/**\n * Returns whether the given event has been handled with the given markName.\n *\n * @param {Event} ev\n * @param {string} markName\n * @returns {boolean}\n */\nexport function isEventHandled(ev, markName) {\n    if (!eventHandledWeakMap.get(ev)) {\n        return false;\n    }\n    return eventHandledWeakMap.get(ev).includes(markName);\n}\n/**\n * Marks the given event as handled by the given markName. Useful to allow\n * handlers in the propagation chain to make a decision based on what has\n * already been done.\n *\n * @param {Event} ev\n * @param {string} markName\n */\nexport function markEventHandled(ev, markName) {\n    if (!eventHandledWeakMap.get(ev)) {\n        eventHandledWeakMap.set(ev, []);\n    }\n    eventHandledWeakMap.get(ev).push(markName);\n}\n", "import { localization } from \"@web/core/l10n/localization\";\nimport { makeDraggableHook } from \"@web/core/utils/draggable_hook_builder_owl\";\n\n/** @typedef {import(\"@web/core/utils/draggable_hook_builder\").DraggableHandlerParams} DraggableHandlerParams */\n/** @typedef {DraggableHandlerParams & { group: HTMLElement | null }} NestedSortableHandlerParams */\n\n/**\n * @typedef {import(\"./sortable\").SortableParams} NestedSortableParams\n *\n * OPTIONAL\n *\n * @property {(HTMLElement) => boolean} [preventDrag] function receiving a\n *  the current target for dragging (element) and returning a boolean, whether\n *  the element can be effectively dragged or not.\n * @property {boolean | () => boolean} [nest] whether elements are nested or not.\n * @property {string | () => string} [listTagName] type of lists (\"ul\" or \"ol\").\n * @property {number | () => number} [nestInterval] Horizontal distance needed to trigger\n * a change in the list hierarchy (i.e. changing parent when moving horizontally)\n * @property {number | () => number} [maxLevels] The maximum depth of nested items\n * the list can accept. If set to '0' the levels are unlimited. Default: 0\n * @property {(DraggableHookContext) => boolean} [isAllowed] You can specify a custom function\n * to verify if a drop location is allowed. return True by default\n * @property {boolean} [useElementSize] The placeholder use the dragged element size instead\n * of the small 8px lines. Default:false\n *\n * HANDLERS (also optional)\n *\n * @property {(params: MoveParams) => any} [onMove] called when the element has moved\n * (changed position) (@see MoveParams).\n */\n\n/**\n * @typedef MoveParams\n * @property {HTMLElement} element\n * @property {HTMLElement | null} group\n * @property {HTMLElement | null} previous\n * @property {HTMLElement | null} next\n * @property {HTMLElement | null} newGroup\n * @property {HTMLElement | null} parent\n * @property {HTMLElement} placeholder\n */\n\n/**\n * @typedef SortableState\n * @property {boolean} dragging\n */\n\n/** @type {(params: NestedSortableParams) => SortableState} */\nexport const useNestedSortable = makeDraggableHook({\n    name: \"useNestedSortable\",\n    acceptedParams: {\n        groups: [String, Function],\n        connectGroups: [Boolean, Function],\n        nest: [Boolean],\n        listTagName: [String],\n        nestInterval: [Number],\n        maxLevels: [Number],\n        isAllowed: [Function],\n        useElementSize: [Boolean],\n    },\n    defaultParams: {\n        connectGroups: false,\n        currentGroup: null,\n        cursor: \"grabbing\",\n        edgeScrolling: { speed: 20, threshold: 60 },\n        elements: \"li\",\n        groupSelector: null,\n        nest: false,\n        listTagName: \"ul\",\n        nestInterval: 15,\n        maxLevels: 0,\n        isAllowed: (ctx) => true,\n        useElementSize: false,\n    },\n\n    // Set the parameters.\n    onComputeParams({ ctx, params }) {\n        // Group selector\n        ctx.groupSelector = params.groups || null;\n        if (ctx.groupSelector) {\n            ctx.fullSelector = [ctx.groupSelector, ctx.fullSelector].join(\" \");\n        }\n        // Connection across groups\n        ctx.connectGroups = params.connectGroups;\n        // Nested elements\n        ctx.nest = params.nest;\n        // List tag name\n        ctx.listTagName = params.listTagName;\n        // Horizontal distance needed to trigger a change in the list hierarchy\n        // (i.e. changing parent when moving horizontally)\n        ctx.nestInterval = params.nestInterval;\n        ctx.isRTL = localization.direction === \"rtl\";\n        ctx.maxLevels = params.maxLevels || 0;\n        ctx.isAllowed = params.isAllowed ?? (() => true);\n        ctx.useElementSize = params.useElementSize;\n    },\n\n    // Set the current group and create the placeholder row that will take the\n    // place of the moving row.\n    onWillStartDrag({ ctx, addCleanup }) {\n        if (ctx.groupSelector) {\n            ctx.currentGroup = ctx.current.element.closest(ctx.groupSelector);\n            if (!ctx.connectGroups) {\n                ctx.current.container = ctx.currentGroup;\n            }\n        }\n\n        if (ctx.nest) {\n            ctx.prevNestX = ctx.pointer.x;\n        }\n        ctx.current.placeHolder = ctx.current.element.cloneNode(false);\n        ctx.current.placeHolder.removeAttribute(\"id\");\n        ctx.current.placeHolder.classList.add(\"w-100\", \"d-block\");\n        if (ctx.useElementSize) {\n            ctx.current.placeHolder.style.height = getComputedStyle(ctx.current.element).height;\n            ctx.current.placeHolder.classList.add(\"o_nested_sortable_placeholder_realsize\");\n        } else {\n            ctx.current.placeHolder.classList.add(\"o_nested_sortable_placeholder\");\n        }\n        addCleanup(() => ctx.current.placeHolder.remove());\n    },\n\n    // Make the placeholder take the place of the moving row, and add style on\n    // different elements to provide feedback that there is an ongoing dragging\n    // sequence.\n    onDragStart({ ctx, addStyle }) {\n        // Horizontal position which will be used to detect row changes when moving vertically, so that\n        // we do not need to be on the row to trigger row changes (only the vertical position matters).\n        // Nested rows are shorter than \"root\" rows, and do not start at the same horizontal position.\n        // However, every row ends at the same horizontal position. Therefore, we use the end of the\n        // current element - 1 as horizontal position.\n        ctx.selectorX = ctx.isRTL\n            ? ctx.current.elementRect.left + 1\n            : ctx.current.elementRect.right - 1;\n\n        // Placeholder is initially added right after the current element.\n        ctx.current.element.after(ctx.current.placeHolder);\n        addStyle(ctx.current.element, { opacity: 0.5 });\n\n        // Remove pointer-events style added by draggable_hook_builder and set\n        // it on the view elements instead as in our case we want to show the\n        // ctx.cursor style on the whole screen, not only in the ref el.\n        addStyle(document.body, { \"pointer-events\": \"auto\" });\n        addStyle(document.querySelector(\".o_navbar\"), { \"pointer-events\": \"none\" });\n        addStyle(document.querySelector(\".o_action_manager\"), { \"pointer-events\": \"none\" });\n        addStyle(ctx.current.container, { \"pointer-events\": \"auto\" });\n\n        // Calls \"onDragStart\" handler\n        return {\n            element: ctx.current.element,\n            group: ctx.currentGroup,\n        };\n    },\n    _getDeepestChildLevel(ctx, node, depth = 0) {\n        let result = 0;\n        const childSelector = `${ctx.listTagName} ${ctx.elementSelector}`;\n        for (const childNode of node.querySelectorAll(childSelector)) {\n            result = Math.max(this._getDeepestChildLevel(ctx, childNode, depth + 1), result);\n        }\n        return depth ? result + 1 : result;\n    },\n    _hasReachMaxAllowedLevel(ctx) {\n        if (!ctx.nest || ctx.maxLevels < 1) {\n            return false;\n        }\n        let level = this._getDeepestChildLevel(ctx, ctx.current.element);\n        let list = ctx.current.placeHolder.closest(ctx.listTagName);\n        while (list) {\n            level++;\n            list = list.parentNode.closest(ctx.listTagName);\n        }\n        return level > ctx.maxLevels;\n    },\n    _isAllowedNodeMove(ctx) {\n        return (\n            !this._hasReachMaxAllowedLevel(ctx) && ctx.isAllowed(ctx.current, ctx.elementSelector)\n        );\n    },\n    // Check if the cursor moved enough to trigger a move. If it did, move the\n    // placeholder accordingly.\n    onDrag({ ctx, callHandler }) {\n        const onMove = (prevPos) => {\n            if (!this._isAllowedNodeMove(ctx)) {\n                ctx.current.placeHolder.classList.add(\"d-none\");\n                return;\n            }\n            ctx.current.placeHolder.classList.remove(\"d-none\");\n            callHandler(\"onMove\", {\n                element: ctx.current.element,\n                previous: ctx.current.placeHolder.previousElementSibling,\n                next: ctx.current.placeHolder.nextElementSibling,\n                parent: ctx.nest\n                    ? ctx.current.placeHolder.parentElement.closest(ctx.elementSelector)\n                    : false,\n                group: ctx.currentGroup,\n                newGroup: ctx.connectGroups\n                    ? ctx.current.placeHolder.closest(ctx.groupSelector)\n                    : ctx.currentGroup,\n                prevPos,\n                placeholder: ctx.current.placeHolder,\n            });\n        };\n        /**\n         * Get the list element inside an element, or create one if it does not\n         * exists.\n         * @param {HTMLElement} el\n         * @return {HTMLElement} list\n         */\n        const getChildList = (el) => {\n            let list = el.querySelector(ctx.listTagName);\n            if (!list) {\n                list = document.createElement(ctx.listTagName);\n                el.appendChild(list);\n            }\n            return list;\n        };\n\n        const getPosition = (el) => {\n            return {\n                previous: el.previousElementSibling,\n                next: el.nextElementSibling,\n                parent: el.parentElement?.closest(ctx.elementSelector) || null,\n                group: ctx.groupSelector ? el.closest(ctx.groupSelector) : false,\n            };\n        };\n        const position = getPosition(ctx.current.placeHolder);\n\n        /** If nesting elements is allowed, horizontal moves may change the\n         * parent of the placeholder element (the placeholder does not move\n         * above or under an element, but it changes parent):\n         *\n         * - Moving to the left makes the placeholder a child of the previous\n         *   element up in the nested hierarchy, only if the placeholder is the\n         *   last child of its current parent:\n         *\n         *                    Allowed:\n         *    el                           el\n         *     \u2523 parent                     \u2523 parent\n         *     \u2503  \u2523 child           -->     \u2503  \u2517 child\n         *     \u2503  \u2517 placeholder             \u2523 placeholder\n         *     \u2517 el                         \u2517 el\n         *\n         *                  Not Allowed:\n         *    el                           el\n         *     \u2523 parent                     \u2523 parent\n         *     \u2503  \u2523 placeholder     -->     \u2523 p\u2503laceholder   <-- error\n         *     \u2503  \u2517 child                   \u2503  \u2517 child\n         *     \u2517 el                         \u2517 el\n         *\n         *\n         * - Moving to the right makes the placeholder the last child of the\n         * next element down in the nested hierarchy:\n         *\n         *    el                           el\n         *     \u2523 parent                    \u2523 parent\n         *     \u2503  \u2517 child           -->    \u2503  \u2523 child\n         *     \u2523 placeholder               \u2503  \u2517 placeholder\n         *     \u2517 el                        \u2517 el\n         */\n        if (ctx.nest) {\n            const xInterval = ctx.prevNestX - ctx.pointer.x;\n            if (ctx.nestInterval - (-1) ** ctx.isRTL * xInterval < 1) {\n                // Place placeholder after its parent in its parent's list only\n                // if the placeholder is the last child of its parent\n                // (ignoring the current element which is in the dom)\n                let nextElement = position.next;\n                if (nextElement === ctx.current.element) {\n                    nextElement = nextElement.nextElementSibling;\n                }\n                if (!nextElement) {\n                    const newSibling = position.parent;\n                    if (newSibling) {\n                        newSibling.after(ctx.current.placeHolder);\n                        onMove(position);\n                    }\n                }\n                // Recenter the pointer coordinates to this step\n                ctx.prevNestX = ctx.pointer.x;\n                return;\n            } else if (ctx.nestInterval + (-1) ** ctx.isRTL * xInterval < 1) {\n                // Place placeholder as the last child of its previous sibling,\n                // (ignoring the current element which is in the dom)\n                let parent = position.previous;\n                if (parent === ctx.current.element) {\n                    parent = parent.previousElementSibling;\n                }\n                if (parent && parent.matches(ctx.elementSelector)) {\n                    getChildList(parent).appendChild(ctx.current.placeHolder);\n                    onMove(position);\n                }\n                // Recenter the pointer coordinates to this step\n                ctx.prevNestX = ctx.pointer.x;\n                return;\n            }\n        }\n        const currentTop = ctx.pointer.y - ctx.current.offset.y;\n        const closestEl = document.elementFromPoint(ctx.selectorX, currentTop);\n        if (!closestEl) {\n            // Cursor outside of viewport\n            return;\n        }\n        const element = closestEl.closest(ctx.elementSelector);\n        // Vertical moves should move the placeholder element up or down.\n        if (element && element !== ctx.current.placeHolder) {\n            const elementPosition = getPosition(element);\n            const eRect = element.getBoundingClientRect();\n            const pos = ctx.current.placeHolder.compareDocumentPosition(element);\n            // Place placeholder before the hovered element in its parent's\n            // list. If the cursor is in the upper part of the element and\n            // if the placeholder is currently after or inside the hovered\n            // element. If the position is not allowed but nesting is allowed,\n            // place the placeholder as the last child of the previous sibling\n            // instead.\n            if (currentTop - eRect.y < 10) {\n                if (\n                    pos & Node.DOCUMENT_POSITION_PRECEDING &&\n                    (ctx.nest || elementPosition.parent === position.parent)\n                ) {\n                    element.before(ctx.current.placeHolder);\n                    onMove(position);\n                    // Recenter the pointer coordinates to this step\n                    ctx.prevNestX = ctx.pointer.x;\n                }\n            } else if (currentTop - eRect.y > 15 && pos === Node.DOCUMENT_POSITION_FOLLOWING) {\n                // Place placeholder after the hovered element in its parent's\n                // list if the cursor is not in the upper part of the\n                // element and if the placeholder is currently before the\n                // hovered element.\n                // If nesting is allowed and if the element has at least one\n                // child, place the placeholder above the first child of the\n                // hovered element instead.\n                if (ctx.nest) {\n                    const elementChildList = getChildList(element);\n                    if (elementChildList.querySelector(ctx.elementSelector)) {\n                        elementChildList.prepend(ctx.current.placeHolder);\n                        onMove(position);\n                    } else {\n                        element.after(ctx.current.placeHolder);\n                        onMove(position);\n                    }\n                    // Recenter the pointer coordinates to this step\n                    ctx.prevNestX = ctx.pointer.x;\n                } else if (elementPosition.parent === position.parent) {\n                    element.after(ctx.current.placeHolder);\n                    onMove(position);\n                }\n            }\n        } else {\n            const group = closestEl.closest(ctx.groupSelector);\n            if (group && group !== position.group && (ctx.nest || !position.parent)) {\n                if (\n                    group.compareDocumentPosition(position.group) ===\n                    Node.DOCUMENT_POSITION_PRECEDING\n                ) {\n                    getChildList(group).prepend(ctx.current.placeHolder);\n                    onMove(position);\n                } else {\n                    getChildList(group).appendChild(ctx.current.placeHolder);\n                    onMove(position);\n                }\n                // Recenter the pointer coordinates to this step\n                ctx.prevNestX = ctx.pointer.x;\n                callHandler(\"onGroupEnter\", { group, placeholder: ctx.current.placeHolder });\n                callHandler(\"onGroupLeave\", {\n                    group: position.group,\n                    placeholder: ctx.current.placeHolder,\n                });\n            }\n        }\n    },\n    // If the drop position is different from the starting position, run the\n    // onDrop handler from the parameters.\n    onDrop({ ctx }) {\n        if (!this._isAllowedNodeMove(ctx)) {\n            return;\n        }\n        const previous = ctx.current.placeHolder.previousElementSibling;\n        const next = ctx.current.placeHolder.nextElementSibling;\n        if (previous !== ctx.current.element && next !== ctx.current.element) {\n            return {\n                element: ctx.current.element,\n                group: ctx.currentGroup,\n                previous,\n                next,\n                newGroup: ctx.groupSelector && ctx.current.placeHolder.closest(ctx.groupSelector),\n                parent: ctx.current.placeHolder.parentElement.closest(ctx.elementSelector),\n                placeholder: ctx.current.placeHolder,\n            };\n        }\n    },\n    // Run the onDragEnd handler from the parameters.\n    onDragEnd({ ctx }) {\n        return {\n            element: ctx.current.element,\n            group: ctx.currentGroup,\n        };\n    },\n});\n", "import { localization as l10n } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { intersperse } from \"@web/core/utils/strings\";\n\n/**\n * Returns value clamped to the inclusive range of min and max.\n *\n * @param {number} num\n * @param {number} min\n * @param {number} max\n * @returns {number}\n */\nexport function clamp(num, min, max) {\n    return Math.max(Math.min(num, max), min);\n}\n\n/**\n * A function to create flexibly-numbered lists of integers, handy for each and map loops.\n * step defaults to 1.\n * Returns a list of integers from start (inclusive) to stop (exclusive), incremented (or decremented) by step.\n * @param {number} start default 0\n * @param {number} stop\n * @param {number} step default 1\n * @returns {number[]}\n */\nexport function range(start, stop, step = 1) {\n    const array = [];\n    const nsteps = Math.floor((stop - start) / step);\n    for (let i = 0; i < nsteps; i++) {\n        array.push(start + step * i);\n    }\n    return array;\n}\n\n/**\n * Returns `value` rounded with `precision`, minimizing IEEE-754 floating point\n * representation errors, and applying the tie-breaking rule selected with\n * `method`, by default \"HALF-UP\" (away from zero).\n *\n * @param {number} value the value to be rounded\n * @param {number} precision a precision parameter. eg: 0.01 rounds to two digits.\n * @param {\"HALF-UP\" | \"HALF-DOWN\" | \"HALF-EVEN\" | \"UP\" | \"DOWN\"} [method=\"HALF-UP\"] the rounding method used:\n *    - \"HALF-UP\" rounds to the closest number with ties going away from zero.\n *    - \"HALF-DOWN\" rounds to the closest number with ties going towards zero.\n *    - \"HALF-EVEN\" rounds to the closest number with ties going to the closest even number.\n *    - \"UP\" always rounds away from 0.\n *    - \"DOWN\" always rounds towards 0.\n */\nexport function roundPrecision(value, precision, method = \"HALF-UP\") {\n    if (!value) {\n        return 0;\n    } else if (!precision || precision < 0) {\n        precision = 1;\n    }\n    let roundingFactor = precision;\n    let normalize = (val) => val / roundingFactor;\n    let denormalize = (val) => val * roundingFactor;\n    // inverting small rounding factors reduces rounding errors\n    if (roundingFactor < 1) {\n        roundingFactor = invertFloat(roundingFactor);\n        [normalize, denormalize] = [denormalize, normalize];\n    }\n    const normalizedValue = normalize(value);\n    const sign = Math.sign(normalizedValue);\n    const epsilonMagnitude = Math.log2(Math.abs(normalizedValue));\n    const epsilon = Math.pow(2, epsilonMagnitude - 50);\n    let roundedValue;\n\n    switch (method) {\n        case \"DOWN\": {\n            roundedValue = Math.trunc(normalizedValue + sign * epsilon);\n            break;\n        }\n        case \"HALF-DOWN\": {\n            roundedValue = Math.round(normalizedValue - sign * epsilon);\n            break;\n        }\n        case \"HALF-UP\": {\n            roundedValue = Math.round(normalizedValue + sign * epsilon);\n            break;\n        }\n        case \"HALF-EVEN\": {\n            const integral = Math.floor(normalizedValue);\n            const remainder = Math.abs(normalizedValue - integral);\n            const isHalf = Math.abs(0.5 - remainder) < epsilon;\n            roundedValue = isHalf ? integral + (integral & 1) : Math.round(normalizedValue);\n            break;\n        }\n        case \"UP\": {\n            roundedValue = Math.trunc(normalizedValue + sign * (1 - epsilon));\n            break;\n        }\n        default: {\n            throw new Error(`Unknown rounding method: ${method}`);\n        }\n    }\n\n    return denormalize(roundedValue);\n}\n\nexport function roundDecimals(value, decimals) {\n    /**\n     * The following decimals introduce numerical errors:\n     * Math.pow(10, -4) = 0.00009999999999999999\n     * Math.pow(10, -5) = 0.000009999999999999999\n     *\n     * Such errors will propagate in roundPrecision and lead to inconsistencies between Python\n     * and JavaScript. To avoid this, we parse the scientific notation.\n     */\n    return roundPrecision(value, parseFloat(\"1e\" + -decimals));\n}\n\n/**\n * @param {number} value\n * @param {integer} decimals\n * @returns {boolean}\n */\nexport function floatIsZero(value, decimals) {\n    return value === 0 || roundDecimals(value, decimals) === 0;\n}\n\n/**\n * Inserts \"thousands\" separators in the provided number.\n *\n * @param {string} string representing integer number\n * @param {string} [thousandsSep=\",\"] the separator to insert\n * @param {number[]} [grouping=[]]\n *   array of relative offsets at which to insert `thousandsSep`.\n *   See `strings.intersperse` method.\n * @returns {string}\n */\nexport function insertThousandsSep(number, thousandsSep = \",\", grouping = []) {\n    const negative = number[0] === \"-\";\n    number = negative ? number.slice(1) : number;\n    return (negative ? \"-\" : \"\") + intersperse(number, grouping, thousandsSep);\n}\n\n/**\n * Format a number to a human readable format. For example, 3000 could become 3k.\n * Or massive number can use the scientific exponential notation.\n *\n * @param {number} number to format\n * @param {Object} [options] Options to format\n * @param {number} [options.decimals=0] number of decimals to use\n *    if minDigits > 1 is used and effective on the number then decimals\n *    will be shrunk to zero, to avoid displaying irrelevant figures ( 0.01 compared to 1000 )\n * @param {number} [options.minDigits=1]\n *    the minimum number of digits to preserve when switching to another\n *    level of thousands (e.g. with a value of '2', 4321 will still be\n *    represented as 4321 otherwise it will be down to one digit (4k))\n * @returns {string}\n */\nexport function humanNumber(number, options = { decimals: 0, minDigits: 1 }) {\n    const decimals = options.decimals || 0;\n    const minDigits = options.minDigits || 1;\n    const d2 = Math.pow(10, decimals);\n    const numberMagnitude = +number.toExponential().split(\"e+\")[1];\n    number = Math.round(number * d2) / d2;\n    // the case numberMagnitude >= 21 corresponds to a number\n    // better expressed in the scientific format.\n    if (numberMagnitude >= 21) {\n        // we do not use number.toExponential(decimals) because we want to\n        // avoid the possible useless O decimals: 1e.+24 preferred to 1.0e+24\n        number = Math.round(number * Math.pow(10, decimals - numberMagnitude)) / d2;\n        return `${number}e+${numberMagnitude}`;\n    }\n    // note: we need to call toString here to make sure we manipulate the resulting\n    // string, not an object with a toString method.\n    const unitSymbols = _t(\"kMGTPE\").toString();\n    const sign = Math.sign(number);\n    number = Math.abs(number);\n    let symbol = \"\";\n    for (let i = unitSymbols.length; i > 0; i--) {\n        const s = Math.pow(10, i * 3);\n        if (s <= number / Math.pow(10, minDigits - 1)) {\n            number = Math.round((number * d2) / s) / d2;\n            symbol = unitSymbols[i - 1];\n            break;\n        }\n    }\n    const { decimalPoint, grouping, thousandsSep } = l10n;\n\n    // determine if we should keep the decimals (we don't want to display 1,020.02k for 1020020)\n    const decimalsToKeep = number >= 1000 ? 0 : decimals;\n    number = sign * number;\n    const [integerPart, decimalPart] = number.toFixed(decimalsToKeep).split(\".\");\n    const int = insertThousandsSep(integerPart, thousandsSep, grouping);\n    if (!decimalPart) {\n        return int + symbol;\n    }\n    return int + decimalPoint + decimalPart + symbol;\n}\n\n/**\n * Returns a string representing a float.  The result takes into account the\n * user settings (to display the correct decimal separator).\n *\n * @param {number} value the value that should be formatted\n * @param {Object} [options]\n * @param {number[]} [options.digits] the number of digits that should be used,\n *   instead of the default digits precision in the field.\n * @param {boolean} [options.humanReadable] if true, large numbers are formatted\n *   to a human readable format.\n * @param {string} [options.decimalPoint] decimal separating character\n * @param {string} [options.thousandsSep] thousands separator to insert\n * @param {number[]} [options.grouping] array of relative offsets at which to\n *   insert `thousandsSep`. See `insertThousandsSep` method.\n * @param {number} [options.decimals] used for humanNumber formmatter\n * @param {boolean} [options.trailingZeros=true] if false, the decimal part\n *   won't contain unnecessary trailing zeros.\n * @returns {string}\n */\nexport function formatFloat(value, options = {}) {\n    if (options.humanReadable) {\n        return humanNumber(value, options);\n    }\n    const grouping = options.grouping || l10n.grouping;\n    const thousandsSep = \"thousandsSep\" in options ? options.thousandsSep : l10n.thousandsSep;\n    const decimalPoint = \"decimalPoint\" in options ? options.decimalPoint : l10n.decimalPoint;\n    let precision;\n    if (options.digits && options.digits[1] !== undefined) {\n        precision = options.digits[1];\n    } else {\n        precision = 2;\n    }\n    const formatted = value.toFixed(precision).split(\".\");\n    formatted[0] = insertThousandsSep(formatted[0], thousandsSep, grouping);\n    if (options.trailingZeros === false && formatted[1]) {\n        formatted[1] = formatted[1].replace(/0+$/, \"\");\n    }\n    return formatted[1] ? formatted.join(decimalPoint) : formatted[0];\n}\n\nconst _INVERTDICT = Object.freeze({\n    1e-1: 1e+1, 1e-2: 1e+2, 1e-3: 1e+3, 1e-4: 1e+4, 1e-5: 1e+5,\n    1e-6: 1e+6, 1e-7: 1e+7, 1e-8: 1e+8, 1e-9: 1e+9, 1e-10: 1e+10,\n    2e-1: 5e+0, 2e-2: 5e+1, 2e-3: 5e+2, 2e-4: 5e+3, 2e-5: 5e+4,\n    2e-6: 5e+5, 2e-7: 5e+6, 2e-8: 5e+7, 2e-9: 5e+8, 2e-10: 5e+9,\n    5e-1: 2e+0, 5e-2: 2e+1, 5e-3: 2e+2, 5e-4: 2e+3, 5e-5: 2e+4,\n    5e-6: 2e+5, 5e-7: 2e+6, 5e-8: 2e+7, 5e-9: 2e+8, 5e-10: 2e+9,\n});\n\n/**\n * Invert a number with increased accuracy.\n *\n * @param {number} value\n * @returns {number}\n */\nexport function invertFloat(value) {\n    let res = _INVERTDICT[value];\n    if (res === undefined) {\n        const [coeff, expt] = value.toExponential().split(\"e\").map(Number.parseFloat);\n        res = Number.parseFloat(`${coeff}e${-expt}`) / Math.pow(coeff, 2);\n    }\n    return res;\n}\n", "/**\n * Shallow compares two objects.\n *\n * @template {unknown} T\n * @param {T} obj1\n * @param {T} obj2\n * @param {(a: T[keyof T], b: T[keyof T]) => boolean} [comparisonFn]\n */\nexport function shallowEqual(obj1, obj2, comparisonFn = (a, b) => a === b) {\n    if (!isObject(obj1) || !isObject(obj2)) {\n        return obj1 === obj2;\n    }\n    const obj1Keys = Reflect.ownKeys(obj1);\n    return (\n        obj1Keys.length === Reflect.ownKeys(obj2).length &&\n        obj1Keys.every((key) => comparisonFn(obj1[key], obj2[key]))\n    );\n}\n\n/**\n * Deeply compares two objects.\n *\n * @template {unknown} T\n * @param {T} obj1\n * @param {T} obj2\n */\nexport const deepEqual = (obj1, obj2) => shallowEqual(obj1, obj2, deepEqual);\n\n/**\n * Deep copies an object. As it relies on JSON this function as some limitations\n * - no support for circular objects\n * - no support for specific classes, that will at best be lost and at worst crash (Map, Set etc...)\n * @template T\n * @param {T} object An object that is fully JSON stringifiable\n * @return {T}\n */\nexport function deepCopy(object) {\n    return object && JSON.parse(JSON.stringify(object));\n}\n\n/**\n * @param {unknown} object\n */\nexport function isObject(object) {\n    return !!object && (typeof object === \"object\" || typeof object === \"function\");\n}\n\n/**\n * Returns a shallow copy of object with every property in properties removed\n * if present in object.\n *\n * @template T\n * @template {keyof T} K\n * @param {T} object\n * @param {K[]} properties\n */\nexport function omit(object, ...properties) {\n    /** @type {Omit<T, K>} */\n    const result = {};\n    const propertiesSet = new Set(properties);\n    for (const key in object) {\n        if (!propertiesSet.has(key)) {\n            result[key] = object[key];\n        }\n    }\n    return result;\n}\n\n/**\n * @template T\n * @template {keyof T} K\n * @param {T} object\n * @param {K[]} properties\n * @returns {Pick<T, K>}\n */\nexport function pick(object, ...properties) {\n    return Object.fromEntries(\n        properties.filter((prop) => prop in object).map((prop) => [prop, object[prop]])\n    );\n}\n\n/**\n * Deeply merges two objects, recursively combining properties.\n * Works like the spread operator but will merge nested objects.\n *\n * This function doesn't merge arrays.\n *\n * @param {Object} target - The target object to merge into.\n * @param {Object} extension - The extension to apply.\n * @returns {Object} - The merged object.\n *\n * @example\n * const target = { a: 1, b: { c: 2 } };\n * const source = { a: 2, b: { d: 3 } };\n * const output = deepMerge(target, source);\n * // output => { a: 2, b: { c: 2, d: 3 } }\n */\nexport function deepMerge(target, extension) {\n    if (!isObject(target) && !isObject(extension)) {\n        return;\n    }\n\n    target = target || {};\n    const output = Object.assign({}, target);\n    if (isObject(extension)) {\n        for (const key of Reflect.ownKeys(extension)) {\n            if (\n                key in target &&\n                isObject(extension[key]) &&\n                !Array.isArray(extension[key]) &&\n                typeof extension[key] !== \"function\"\n            ) {\n                output[key] = deepMerge(target[key], extension[key]);\n            } else {\n                Object.assign(output, { [key]: extension[key] });\n            }\n        }\n    }\n\n    return output;\n}\n", "/**\n *  @typedef {{\n *      originalProperties: Map<string, PropertyDescriptor>;\n *      skeleton: object;\n *      extensions: Set<object>;\n *  }} PatchDescription\n */\n\n/** @type {WeakMap<object, PatchDescription>} */\nconst patchDescriptions = new WeakMap();\n\n/**\n * Create or get the patch description for the given `objToPatch`.\n * @param {object} objToPatch\n * @returns {PatchDescription}\n */\nfunction getPatchDescription(objToPatch) {\n    if (!patchDescriptions.has(objToPatch)) {\n        patchDescriptions.set(objToPatch, {\n            originalProperties: new Map(),\n            skeleton: Object.create(Object.getPrototypeOf(objToPatch)),\n            extensions: new Set(),\n        });\n    }\n    return patchDescriptions.get(objToPatch);\n}\n\n/**\n * @param {object} objToPatch\n * @returns {boolean}\n */\nfunction isClassPrototype(objToPatch) {\n    // class A {}\n    // isClassPrototype(A) === false\n    // isClassPrototype(A.prototype) === true\n    // isClassPrototype(new A()) === false\n    // isClassPrototype({}) === false\n    return (\n        Object.hasOwn(objToPatch, \"constructor\") && objToPatch.constructor?.prototype === objToPatch\n    );\n}\n\n/**\n * Traverse the prototype chain to find a potential property.\n * @param {object} objToPatch\n * @param {string} key\n * @returns {object}\n */\nfunction findAncestorPropertyDescriptor(objToPatch, key) {\n    let descriptor = null;\n    let prototype = objToPatch;\n    do {\n        descriptor = Object.getOwnPropertyDescriptor(prototype, key);\n        prototype = Object.getPrototypeOf(prototype);\n    } while (!descriptor && prototype);\n    return descriptor;\n}\n\n/**\n * Patch an object\n *\n * If the intent is to patch a class, don't forget to patch the prototype, unless\n * you want to patch static properties/methods.\n *\n * @template T\n * @template {Partial<T>} U\n * @param {T} objToPatch The object to patch\n * @param {U} extension The object containing the patched properties\n * @returns {() => void} Returns an unpatch function\n */\nexport function patch(objToPatch, extension) {\n    if (typeof extension === \"string\") {\n        throw new Error(\n            `Patch \"${extension}\": Second argument is not the patch name anymore, it should be the object containing the patched properties`\n        );\n    }\n\n    const description = getPatchDescription(objToPatch);\n    description.extensions.add(extension);\n\n    const properties = Object.getOwnPropertyDescriptors(extension);\n    for (const [key, newProperty] of Object.entries(properties)) {\n        const oldProperty = Object.getOwnPropertyDescriptor(objToPatch, key);\n        if (oldProperty) {\n            // Store the old property on the skeleton.\n            Object.defineProperty(description.skeleton, key, oldProperty);\n        }\n\n        if (!description.originalProperties.has(key)) {\n            // Keep a trace of original property (prop before first patch), useful for unpatching.\n            description.originalProperties.set(key, oldProperty);\n        }\n\n        if (isClassPrototype(objToPatch)) {\n            // A property is enumerable on POJO ({ prop: 1 }) but not on classes (class A {}).\n            // Here, we only check if we patch a class prototype.\n            newProperty.enumerable = false;\n        }\n\n        if ((newProperty.get && 1) ^ (newProperty.set && 1)) {\n            // get and set are defined together. If they are both defined\n            // in the previous descriptor but only one in the new descriptor\n            // then the other will be undefined so we need to apply the\n            // previous descriptor in the new one.\n            const ancestorProperty = findAncestorPropertyDescriptor(objToPatch, key);\n            newProperty.get = newProperty.get ?? ancestorProperty?.get;\n            newProperty.set = newProperty.set ?? ancestorProperty?.set;\n        }\n\n        // Replace the old property by the new one.\n        Object.defineProperty(objToPatch, key, newProperty);\n    }\n\n    // Sets the current skeleton as the extension's prototype to make\n    // `super` keyword working and then set extension as the new skeleton.\n    description.skeleton = Object.setPrototypeOf(extension, description.skeleton);\n\n    return () => {\n        // Remove the description to start with a fresh base.\n        patchDescriptions.delete(objToPatch);\n\n        for (const [key, property] of description.originalProperties) {\n            if (property) {\n                // Restore the original property on the `objToPatch` object.\n                Object.defineProperty(objToPatch, key, property);\n            } else {\n                // Or remove the property if it did not exist at first.\n                delete objToPatch[key];\n            }\n        }\n\n        // Re-apply the patches without the current one.\n        description.extensions.delete(extension);\n        for (const extension of description.extensions) {\n            patch(objToPatch, extension);\n        }\n    };\n}\n", "import { reactive } from \"@odoo/owl\";\n\n/**\n * This class should be used as a base when creating a class that is intended to\n * be used within the reactivity system, it avoids a specific class of bug where\n * callbacks that capture `this` declared in the constructor would escape the\n * reactivity system and prevent the observers from being notified:\n *\n * const bus = new EventBus();\n * class MyClass {\n *   constructor() {\n *     this.counter = 0;\n *     bus.addEventListener(\"change\", () => this.counter++);\n *     //                                   ^ Will never be reactive, this mutation will be missed\n *   }\n * }\n * const myObj = reactive(new MyClass(bus), () => console.log(myObj.counter));\n * myObj.counter++; // logs 0;\n * bus.trigger(\"change\"); // logs nothing!\n * myObj.counter++; // logs 2. counter == 1 was missed.\n */\nexport class Reactive {\n    constructor() {\n        return reactive(this);\n    }\n}\n\n/**\n * Creates a side-effect that runs based on the content of reactive objects.\n *\n * @template {object[]} T\n * @param {(...args: [...T]) => X} cb callback for the effect\n * @param {[...T]} deps the reactive objects that the effect depends on\n */\nexport function effect(cb, deps) {\n    const reactiveDeps = reactive(deps, () => {\n        cb(...reactiveDeps);\n    });\n    cb(...reactiveDeps);\n}\n\n/**\n * Adds computed properties to a reactive object derived from multiples sources.\n *\n * @template {object} T\n * @template {object[]} U\n * @template {{[key: string]: (this: T, ...rest: [...U]) => unknown}} V\n * @param {T} obj the reactive object on which to add the computed\n * properties\n * @param {[...U]} sources the reactive objects which are needed to compute\n * the properties\n * @param {V} descriptor the object containing methods to compute the\n * properties\n * @returns {T & {[key in keyof V]: ReturnType<V[key]>}}\n */\nexport function withComputedProperties(obj, sources, descriptor) {\n    for (const [key, compute] of Object.entries(descriptor)) {\n        effect(\n            (obj, sources) => {\n                obj[key] = compute.call(obj, ...sources);\n            },\n            [obj, sources]\n        );\n    }\n    return obj;\n}\n", "import { App, blockDom, Component, markup } from \"@odoo/owl\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport function renderToElement(template, context = {}) {\n    const el = render(template, context).firstElementChild;\n    if (el?.nextElementSibling) {\n        throw new Error(\n            `The rendered template '${template}' contains multiple root ` +\n                `nodes that will be ignored using renderToElement, you should ` +\n                `consider using renderToFragment or refactoring the template.`\n        );\n    }\n    el?.remove();\n    return el;\n}\n\nexport function renderToFragment(template, context = {}) {\n    const frag = document.createDocumentFragment();\n    for (const el of [...render(template, context).children]) {\n        frag.appendChild(el);\n    }\n    return frag;\n}\n\n/**\n * renders a template with an (optional) context and outputs it as a string\n *\n * @param {string} template\n * @param {Object} context\n * @returns string: the html of the template\n */\nexport function renderToString(template, context = {}) {\n    return render(template, context).innerHTML;\n}\nlet app;\nObject.defineProperty(renderToString, \"app\", {\n    get: () => {\n        if (!app) {\n            app = new App(Component, {\n                name: \"renderToString\",\n                getTemplate,\n                translatableAttributes: [\"data-tooltip\"],\n                translateFn: _t,\n            });\n        }\n        return app;\n    },\n});\n\nfunction render(template, context = {}) {\n    const app = renderToString.app;\n    const templateFn = app.getTemplate(template);\n    const bdom = templateFn(context, {});\n    const div = document.createElement(\"div\");\n    blockDom.mount(bdom, div);\n    return div;\n}\n\n/**\n * renders a template with an (optional) context and returns a Markup string,\n * suitable to be inserted in a template with a t-out directive\n *\n * @param {string} template\n * @param {Object} context\n * @returns string: the html of the template, as a markup string\n */\nexport function renderToMarkup(template, context = {}) {\n    return markup(renderToString(template, context));\n}\n", "export function isScrollableX(el) {\n    if (el.scrollWidth > el.clientWidth && el.clientWidth > 0) {\n        return couldBeScrollableX(el);\n    }\n    return false;\n}\n\nexport function couldBeScrollableX(el) {\n    if (el) {\n        const overflow = getComputedStyle(el).getPropertyValue(\"overflow-x\");\n        if (/\\bauto\\b|\\bscroll\\b/.test(overflow)) {\n            return true;\n        }\n    }\n    return false;\n}\n\n/**\n * Get the closest horizontally scrollable for a given element.\n *\n * @param {HTMLElement} el\n * @returns {HTMLElement | null}\n */\nexport function closestScrollableX(el) {\n    if (!el) {\n        return null;\n    }\n    if (isScrollableX(el)) {\n        return el;\n    }\n    return closestScrollableX(el.parentElement);\n}\n\nexport function isScrollableY(el) {\n    if (el && el.scrollHeight > el.clientHeight && el.clientHeight > 0) {\n        return couldBeScrollableY(el);\n    }\n    return false;\n}\n\nexport function couldBeScrollableY(el) {\n    if (el) {\n        const overflow = getComputedStyle(el).getPropertyValue(\"overflow-y\");\n        if (/\\bauto\\b|\\bscroll\\b/.test(overflow)) {\n            return true;\n        }\n    }\n    return false;\n}\n\n/**\n * Get the closest vertically scrollable for a given element.\n *\n * @param {HTMLElement} el\n * @returns {HTMLElement | null}\n */\nexport function closestScrollableY(el) {\n    if (!el) {\n        return null;\n    }\n    if (isScrollableY(el)) {\n        return el;\n    }\n    return closestScrollableY(el.parentElement);\n}\n\n/**\n * Ensures that `element` will be visible in its `scrollable`.\n *\n * @param {HTMLElement} element\n * @param {object} options\n * @param {HTMLElement} [options.scrollable] a scrollable area\n * @param {boolean} [options.isAnchor] states if the scroll is to an anchor\n * @param {string} [options.behavior] \"smooth\", \"instant\", \"auto\" <=> undefined\n *        @url https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo#behavior\n * @param {number} [options.offset] applies a vertical offset\n */\nexport function scrollTo(element, options = {}) {\n    const { behavior = \"auto\", isAnchor = false, offset = 0 } = options;\n    const scrollable = closestScrollableY(options.scrollable || element.parentElement);\n    if (!scrollable) {\n        return;\n    }\n\n    const scrollBottom = scrollable.getBoundingClientRect().bottom;\n    const scrollTop = scrollable.getBoundingClientRect().top;\n    const elementBottom = element.getBoundingClientRect().bottom;\n    const elementTop = element.getBoundingClientRect().top;\n\n    const scrollPromises = [];\n\n    if (elementBottom > scrollBottom && !isAnchor) {\n        // The scroll place the element at the bottom border of the scrollable\n        scrollPromises.push(\n            new Promise((resolve) => {\n                scrollable.addEventListener(\"scrollend\", () => resolve(), { once: true });\n            })\n        );\n\n        scrollable.scrollTo({\n            top:\n                scrollable.scrollTop +\n                elementTop -\n                scrollBottom +\n                Math.ceil(element.getBoundingClientRect().height) +\n                offset,\n            behavior,\n        });\n    } else if (elementTop < scrollTop || isAnchor) {\n        // The scroll place the element at the top of the scrollable\n        scrollPromises.push(\n            new Promise((resolve) => {\n                scrollable.addEventListener(\"scrollend\", () => resolve(), { once: true });\n            })\n        );\n\n        scrollable.scrollTo({\n            top: scrollable.scrollTop - scrollTop + elementTop + offset,\n            behavior,\n        });\n\n        if (options.isAnchor) {\n            // If the scrollable is within a scrollable, another scroll should be done\n            const parentScrollable = closestScrollableY(scrollable.parentElement);\n            if (parentScrollable) {\n                scrollPromises.push(\n                    scrollTo(scrollable, {\n                        behavior,\n                        isAnchor: true,\n                        scrollable: parentScrollable,\n                    })\n                );\n            }\n        }\n    }\n\n    return Promise.all(scrollPromises);\n}\n\nexport function compensateScrollbar(\n    el,\n    add = true,\n    isScrollElement = true,\n    cssProperty = \"padding-right\"\n) {\n    if (!el) {\n        return;\n    }\n    // Compensate scrollbar\n    const scrollableEl = isScrollElement ? el : closestScrollableY(el.parentElement);\n    if (!scrollableEl) {\n        return;\n    }\n    const isRTL = scrollableEl.classList.contains(\".o_rtl\");\n    if (isRTL) {\n        cssProperty = cssProperty.replace(\"right\", \"left\");\n    }\n    el.style.removeProperty(cssProperty);\n    if (!add) {\n        return;\n    }\n    const style = window.getComputedStyle(el);\n    // Round up to the nearest integer to be as close as possible to\n    // the correct value in case of browser zoom.\n    const borderLeftWidth = Math.ceil(parseFloat(style.borderLeftWidth.replace(\"px\", \"\")));\n    const borderRightWidth = Math.ceil(parseFloat(style.borderRightWidth.replace(\"px\", \"\")));\n    const bordersWidth = borderLeftWidth + borderRightWidth;\n    const newValue =\n        parseInt(style[cssProperty]) +\n        scrollableEl.offsetWidth -\n        scrollableEl.clientWidth -\n        bordersWidth;\n    el.style.setProperty(cssProperty, `${newValue}px`, \"important\");\n}\n\nexport function getScrollingElement(document = window.document) {\n    const baseScrollingElement = document.scrollingElement;\n    if (isScrollableY(baseScrollingElement)) {\n        return baseScrollingElement;\n    }\n    const bodyHeight = window.getComputedStyle(document.body).height;\n    for (const el of document.body.children) {\n        // Search for a body child which is at least as tall as the body\n        // and which has the ability to scroll if enough content in it. If\n        // found, suppose this is the top scrolling element.\n        if (bodyHeight - el.scrollHeight > 1.5) {\n            continue;\n        }\n        if (isScrollableY(el)) {\n            return el;\n        }\n    }\n    return baseScrollingElement;\n}\n", "import { unaccent } from \"./strings\";\n\n/**\n * @param {string} pattern\n * @param {string|string[]} strs\n * @returns {number}\n */\nfunction match(pattern, strs) {\n    if (!Array.isArray(strs)) {\n        strs = [strs];\n    }\n    let globalScore = 0;\n    for (const str of strs) {\n        globalScore = Math.max(globalScore, _match(pattern, str));\n    }\n    return globalScore;\n}\n\n/**\n * This private function computes a score that represent the fact that the\n * string contains the pattern, or not\n *\n * - If the score is 0, the string does not contain the letters of the pattern in\n *   the correct order.\n * - if the score is > 0, it actually contains the letters.\n *\n * Better matches will get a higher score: consecutive letters are better,\n * and a match closer to the beginning of the string is also scored higher.\n *\n * @param {string} pattern\n * @param {string} str\n * @returns {number}\n */\nfunction _match(pattern, str) {\n    let totalScore = 0;\n    let currentScore = 0;\n    const len = str.length;\n    let patternIndex = 0;\n\n    pattern = unaccent(pattern, false);\n    str = unaccent(str, false);\n\n    for (let i = 0; i < len; i++) {\n        if (str[i] === pattern[patternIndex]) {\n            patternIndex++;\n            currentScore += 100 + currentScore - i / 200;\n        } else {\n            currentScore = 0;\n        }\n        totalScore = totalScore + currentScore;\n    }\n\n    return patternIndex === pattern.length ? totalScore : 0;\n}\n\n/**\n * Return a list of things that matches a pattern, ordered by their 'score' (\n * higher score first). An higher score means that the match is better. For\n * example, consecutive letters are considered a better match.\n *\n * @template T\n * @param {string} pattern\n * @param {T[]} list\n * @param {(element: T) => (string|string[])} fn\n * @returns {T[]}\n */\nexport function fuzzyLookup(pattern, list, fn) {\n    const results = [];\n    list.forEach((data) => {\n        const score = match(pattern, fn(data));\n        if (score > 0) {\n            results.push({ score, elem: data });\n        }\n    });\n\n    // we want better matches first\n    results.sort((a, b) => b.score - a.score);\n\n    return results.map((r) => r.elem);\n}\n\n// Does `pattern` fuzzy match `string`?\n/**\n * @param {string} pattern\n * @param {string} string\n * @returns {boolean}\n */\nexport function fuzzyTest(pattern, string) {\n    return _match(pattern, string) !== 0;\n}\n", "import {\n    DRAGGED_CLASS,\n    makeDraggableHook as nativeMakeDraggableHook,\n} from \"@web/core/utils/draggable_hook_builder\";\nimport { pick } from \"@web/core/utils/objects\";\n\n/** @typedef {import(\"@web/core/utils/draggable_hook_builder\").DraggableHandlerParams} DraggableHandlerParams */\n/** @typedef {DraggableHandlerParams & { group: HTMLElement | null }} SortableHandlerParams */\n\n/**\n * @typedef SortableParams\n *\n * MANDATORY\n *\n * @property {{ el: HTMLElement | null }} ref\n * @property {string} elements defines sortable elements\n *\n * OPTIONAL\n *\n * @property {boolean | (() => boolean)} [enable] whether the sortable system should\n *  be enabled.\n * @property {number} [delay] delay before starting a sequence after a \"pointerdown\".\n * @property {number} [touchDelay] same as \"delay\", but specific to touch environments.\n * @property {string | (() => string)} [groups] defines parent groups of sortable\n *  elements. This allows to add `onGroupEnter` and `onGroupLeave` callbacks to\n *  work on group elements during the dragging sequence.\n * @property {string | (() => string)} [handle] additional selector for when the\n *  dragging sequence must be initiated when dragging on a certain part of the element.\n * @property {string | (() => string)} [ignore] selector targetting elements that\n *  must initiate a drag.\n * @property {boolean | (() => boolean)} [connectGroups] whether elements can be\n *  dragged accross different parent groups. Note that it requires a `groups` param to work.\n * @property {string | (() => string)} [cursor] cursor style during the dragging\n *  sequence.\n * @property {boolean} [clone] the placeholder is a clone of the drag element.\n * @property {string[]} [placeholderClasses] array of classes added to the placeholder\n *  element.\n * @property {boolean} [applyChangeOnDrop] on drop the change is applied to the DOM.\n * @property {string[]} [followingElementClasses] array of classes added to the\n *  element that follow the pointer.\n *\n * HANDLERS (also optional)\n *\n * @property {(params: SortableHandlerParams) => any} [onDragStart]\n *  called when a dragging sequence is initiated.\n * @property {(params: DraggableHandlerParams) => any} [onElementEnter] called when\n *  the cursor enters another sortable element.\n * @property {(params: DraggableHandlerParams) => any} [onElementLeave] called when\n *  the cursor leaves another sortable element.\n * @property {(params: SortableHandlerParams) => any} [onGroupEnter] (if a `groups`\n *  is specified): will be called when the cursor enters another group element.\n * @property {(params: SortableHandlerParams) => any} [onGroupLeave] (if a `groups`\n *  is specified): will be called when the cursor leaves another group element.\n * @property {(params: SortableHandlerParams) => any} [onDragEnd]\n *  called when the dragging sequence ends, regardless of the reason.\n * @property {(params: DropParams) => any} [onDrop] called when the dragging sequence\n *  ends on a pointerup action AND the dragged element has been moved elsewhere.\n *  The callback will be given an object with any useful element regarding the new\n *  position of the dragged element (@see DropParams ).\n */\n\n/**\n * @typedef DropParams\n * @property {HTMLElement} element\n * @property {HTMLElement | null} group\n * @property {HTMLElement | null} previous\n * @property {HTMLElement | null} next\n * @property {HTMLElement | null} parent\n */\n\n/**\n * @typedef SortableState\n * @property {boolean} dragging\n */\n\n/** @type SortableParams */\nconst hookParams = {\n    name: \"useSortable\",\n    acceptedParams: {\n        groups: [String, Function],\n        connectGroups: [Boolean, Function],\n        clone: [Boolean],\n        placeholderClasses: [Object],\n        applyChangeOnDrop: [Boolean],\n        followingElementClasses: [Object],\n    },\n    defaultParams: {\n        connectGroups: false,\n        edgeScrolling: { speed: 20, threshold: 60 },\n        groupSelector: null,\n        clone: true,\n        placeholderClasses: [],\n        applyChangeOnDrop: false,\n        followingElementClasses: [],\n    },\n\n    // Build steps\n    onComputeParams({ ctx, params }) {\n        // Group selector\n        ctx.groupSelector = params.groups || null;\n        if (ctx.groupSelector) {\n            ctx.fullSelector = [ctx.groupSelector, ctx.fullSelector].join(\" \");\n        }\n\n        // Connection accross groups\n        ctx.connectGroups = params.connectGroups;\n\n        ctx.placeholderClone = params.clone;\n        ctx.placeholderClasses = params.placeholderClasses;\n        ctx.applyChangeOnDrop = params.applyChangeOnDrop;\n        ctx.followingElementClasses = params.followingElementClasses;\n    },\n\n    // Runtime steps\n    onDragStart({ ctx, addListener, addStyle, callHandler }) {\n        /**\n         * Element \"pointerenter\" event handler.\n         * @param {PointerEvent} ev\n         */\n        const onElementPointerEnter = (ev) => {\n            const element = ev.currentTarget;\n            if (\n                connectGroups ||\n                !groupSelector ||\n                current.group === element.closest(groupSelector)\n            ) {\n                const pos = current.placeHolder.compareDocumentPosition(element);\n                if (pos === Node.DOCUMENT_POSITION_PRECEDING) {\n                    element.before(current.placeHolder);\n                } else if (pos === Node.DOCUMENT_POSITION_FOLLOWING) {\n                    element.after(current.placeHolder);\n                }\n            }\n            callHandler(\"onElementEnter\", { element });\n        };\n\n        /**\n         * Element \"pointerleave\" event handler.\n         * @param {PointerEvent} ev\n         */\n        const onElementPointerLeave = (ev) => {\n            const element = ev.currentTarget;\n            callHandler(\"onElementLeave\", { element });\n        };\n\n        const onElementComplexPointerEnter = (ev) => {\n            if (ctx.haveAlreadyChanged) {\n                return;\n            }\n            const element = ev.currentTarget;\n\n            const siblingArray = [...element.parentElement.children].filter(\n                (el) =>\n                    el === current.placeHolder ||\n                    (el.matches(elementSelector) && !el.classList.contains(DRAGGED_CLASS))\n            );\n            const elementIndex = siblingArray.indexOf(element);\n            const placeholderIndex = siblingArray.indexOf(current.placeHolder);\n            const isDirectSibling = Math.abs(elementIndex - placeholderIndex) === 1;\n            if (\n                connectGroups ||\n                !groupSelector ||\n                current.group === element.closest(groupSelector)\n            ) {\n                const pos = current.placeHolder.compareDocumentPosition(element);\n                if (isDirectSibling) {\n                    if (pos === Node.DOCUMENT_POSITION_PRECEDING) {\n                        element.before(current.placeHolder);\n                        ctx.haveAlreadyChanged = true;\n                    } else if (pos === Node.DOCUMENT_POSITION_FOLLOWING) {\n                        element.after(current.placeHolder);\n                        ctx.haveAlreadyChanged = true;\n                    }\n                } else {\n                    if (pos === Node.DOCUMENT_POSITION_FOLLOWING) {\n                        element.before(current.placeHolder);\n                        ctx.haveAlreadyChanged = true;\n                    } else if (pos === Node.DOCUMENT_POSITION_PRECEDING) {\n                        element.after(current.placeHolder);\n                        ctx.haveAlreadyChanged = true;\n                    }\n                }\n            }\n            callHandler(\"onElementEnter\", { element });\n        };\n\n        /**\n         * Element \"pointerleave\" event handler.\n         * @param {PointerEvent} ev\n         */\n        const onElementComplexPointerLeave = (ev) => {\n            if (ctx.haveAlreadyChanged) {\n                return;\n            }\n            const element = ev.currentTarget;\n            const elementRect = element.getBoundingClientRect();\n\n            const relatedElement = ev.relatedTarget;\n            const relatedElementRect = element.getBoundingClientRect();\n\n            const siblingArray = [...element.parentElement.children].filter(\n                (el) =>\n                    el === current.placeHolder ||\n                    (el.matches(elementSelector) && !el.classList.contains(DRAGGED_CLASS))\n            );\n            const pointerOnSiblings = siblingArray.indexOf(relatedElement) > -1;\n            const elementIndex = siblingArray.indexOf(element);\n            const isFirst = elementIndex === 0;\n            const isAbove = relatedElementRect.top <= elementRect.top;\n            const isLast = elementIndex === siblingArray.length - 1;\n            const isBelow = relatedElementRect.bottom >= elementRect.bottom;\n            const pos = current.placeHolder.compareDocumentPosition(element);\n            if (!pointerOnSiblings) {\n                if (isFirst && isAbove && pos === Node.DOCUMENT_POSITION_PRECEDING) {\n                    element.before(current.placeHolder);\n                    ctx.haveAlreadyChanged = true;\n                } else if (isLast && isBelow && pos === Node.DOCUMENT_POSITION_FOLLOWING) {\n                    element.after(current.placeHolder);\n                    ctx.haveAlreadyChanged = true;\n                }\n            }\n            callHandler(\"onElementLeave\", { element });\n        };\n\n        /**\n         * Group \"pointerenter\" event handler.\n         * @param {PointerEvent} ev\n         */\n        const onGroupPointerEnter = (ev) => {\n            const group = ev.currentTarget;\n            group.appendChild(current.placeHolder);\n            callHandler(\"onGroupEnter\", { group });\n        };\n\n        /**\n         * Group \"pointerleave\" event handler.\n         * @param {PointerEvent} ev\n         */\n        const onGroupPointerLeave = (ev) => {\n            const group = ev.currentTarget;\n            callHandler(\"onGroupLeave\", { group });\n        };\n\n        const { connectGroups, current, elementSelector, groupSelector, ref } = ctx;\n        if (ctx.placeholderClone) {\n            const { width, height } = current.elementRect;\n\n            // Adjusts size for the placeholder element\n            addStyle(current.placeHolder, {\n                visibility: \"hidden\",\n                display: \"block\",\n                width: `${width}px`,\n                height: `${height}px`,\n            });\n        }\n\n        // Binds handlers on eligible groups, if the elements are not confined to\n        // their parents and a 'groupSelector' has been provided.\n        if (connectGroups && groupSelector) {\n            for (const siblingGroup of ref.el.querySelectorAll(groupSelector)) {\n                addListener(siblingGroup, \"pointerenter\", onGroupPointerEnter);\n                addListener(siblingGroup, \"pointerleave\", onGroupPointerLeave);\n            }\n        }\n\n        // Binds handlers on eligible elements\n        for (const siblingEl of ref.el.querySelectorAll(elementSelector)) {\n            if (siblingEl !== current.element && siblingEl !== current.placeHolder) {\n                if (ctx.placeholderClone) {\n                    addListener(siblingEl, \"pointerenter\", onElementPointerEnter);\n                    addListener(siblingEl, \"pointerleave\", onElementPointerLeave);\n                } else {\n                    addListener(siblingEl, \"pointerenter\", onElementComplexPointerEnter);\n                    addListener(siblingEl, \"pointerleave\", onElementComplexPointerLeave);\n                }\n            }\n        }\n\n        // Placeholder is initially added right after the current element.\n        current.element.after(current.placeHolder);\n\n        return pick(current, \"element\", \"group\");\n    },\n    onDrag({ ctx }) {\n        ctx.haveAlreadyChanged = false;\n    },\n    onDragEnd({ ctx }) {\n        return pick(ctx.current, \"element\", \"group\");\n    },\n    onDrop({ ctx }) {\n        const { current, groupSelector } = ctx;\n        const previous = current.placeHolder.previousElementSibling;\n        const next = current.placeHolder.nextElementSibling;\n        if (previous !== current.element && next !== current.element) {\n            const element = current.element;\n            if (ctx.applyChangeOnDrop) {\n                // Apply to the DOM the result of sortable()\n                if (previous) {\n                    previous.after(element);\n                } else if (next) {\n                    next.before(element);\n                }\n            }\n            return {\n                element,\n                group: current.group,\n                previous,\n                next,\n                parent: groupSelector && current.placeHolder.closest(groupSelector),\n            };\n        }\n    },\n    onWillStartDrag({ ctx, addCleanup }) {\n        const { connectGroups, current, groupSelector } = ctx;\n\n        if (groupSelector) {\n            current.group = current.element.closest(groupSelector);\n            if (!connectGroups) {\n                current.container = current.group;\n            }\n        }\n\n        if (ctx.placeholderClone) {\n            current.placeHolder = current.element.cloneNode(false);\n        } else {\n            current.placeHolder = document.createElement(\"div\");\n        }\n        current.placeHolder.classList.add(...ctx.placeholderClasses);\n        current.element.classList.add(...ctx.followingElementClasses);\n\n        addCleanup(() => current.element.classList.remove(...ctx.followingElementClasses));\n        addCleanup(() => current.placeHolder.remove());\n\n        return pick(current, \"element\", \"group\");\n    },\n};\n\n/** @type {(params: SortableParams) => SortableState} */\nexport const useSortable = (sortableParams) => {\n    const { setupHooks } = sortableParams;\n    delete sortableParams.setupHooks;\n    return nativeMakeDraggableHook({ ...hookParams, setupHooks })(sortableParams);\n};\n", "import { onWillUnmount, reactive, useEffect, useExternalListener } from \"@odoo/owl\";\nimport { useThrottleForAnimation } from \"./timing\";\nimport { useSortable as nativeUseSortable } from \"@web/core/utils/sortable\";\n\n/**\n * Set of default `useSortable` setup hooks that makes use of Owl lifecycle\n * and reactivity hooks to properly set up, update and tear down the elements and\n * listeners added by the draggable hook builder.\n *\n * @see {nativeUseSortable}\n * @type {typeof nativeUseSortable}\n */\nexport function useSortable(params) {\n    return nativeUseSortable({\n        ...params,\n        setupHooks: {\n            addListener: useExternalListener,\n            setup: useEffect,\n            teardown: onWillUnmount,\n            throttle: useThrottleForAnimation,\n            wrapState: reactive,\n        },\n    });\n}\n", "import { registry } from \"../registry\";\nimport { useSortable } from \"@web/core/utils/sortable\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { reactive } from \"@odoo/owl\";\n\n/**\n * @typedef SortableServiceHookParams\n * @extends SortableParams\n * @property {{el: HTMLElement} | ReturnType<typeof import(\"@odoo/owl\").useRef>} [ref] container of sortable\n * @property {string | Symbol} [sortableId] identifier when multiple sortable on the same container\n */\n\nconst DEFAULT_SORTABLE_ID = Symbol.for(\"defaultSortable\");\nexport const sortableService = {\n    start() {\n        /**\n         * Map to avoid to setup/enable twice or more time the same element\n         * @type {Map<Element, Object>}\n         */\n        const boundElements = new Map();\n        return {\n            /**\n             * @param {SortableServiceHookParams} hookParams\n             */\n            create: (hookParams) => {\n                const element = hookParams.ref.el;\n                const sortableId = hookParams.sortableId ?? DEFAULT_SORTABLE_ID;\n                if (boundElements.has(element)) {\n                    const boundElement = boundElements.get(element);\n                    if (sortableId in boundElement) {\n                        return {\n                            enable() {\n                                return {\n                                    cleanup: boundElement[sortableId],\n                                };\n                            },\n                        };\n                    }\n                }\n                /**\n                 * @type {Map<Function, function():Array>}\n                 */\n                const setupFunctions = new Map();\n                /**\n                 * @type {Array<Function>}\n                 */\n                const cleanupFunctions = [];\n\n                const cleanup = () => {\n                    const boundElement = boundElements.get(element);\n                    if (sortableId in boundElement) {\n                        delete boundElement[sortableId];\n                        if (boundElement.length === 0) {\n                            boundElements.delete(element);\n                        }\n                    }\n                    cleanupFunctions.forEach((fn) => fn());\n                };\n\n                // Setup hookParam\n                const setupHooks = {\n                    wrapState: reactive,\n                    throttle: throttleForAnimation,\n                    addListener: (el, type, listener) => {\n                        el.addEventListener(type, listener);\n                        cleanupFunctions.push(() => el.removeEventListener(type, listener));\n                    },\n                    setup: (setupFn, dependenciesFn) => setupFunctions.set(setupFn, dependenciesFn),\n                    teardown: (fn) => cleanupFunctions.push(fn),\n                };\n\n                useSortable({ setupHooks, ...hookParams });\n\n                const boundElement = boundElements.get(element);\n                if (boundElement) {\n                    boundElement[sortableId] = cleanup;\n                } else {\n                    boundElements.set(element, { [sortableId]: cleanup });\n                }\n\n                return {\n                    enable() {\n                        setupFunctions.forEach((dependenciesFn, setupFn) =>\n                            setupFn(...dependenciesFn())\n                        );\n                        return {\n                            cleanup,\n                        };\n                    },\n                };\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"sortable\", sortableService);\n", "export const nbsp = \"\\u00a0\";\n\n/**\n * Escapes a string for HTML.\n *\n * @param {string | number} [str] the string to escape\n * @returns {string} an escaped string\n */\nexport function escape(str) {\n    if (str === undefined) {\n        return \"\";\n    }\n    if (typeof str === \"number\") {\n        return String(str);\n    }\n    [\n        [\"&\", \"&amp;\"],\n        [\"<\", \"&lt;\"],\n        [\">\", \"&gt;\"],\n        [\"'\", \"&#x27;\"],\n        ['\"', \"&quot;\"],\n        [\"`\", \"&#x60;\"],\n    ].forEach((pairs) => {\n        str = String(str).replaceAll(pairs[0], pairs[1]);\n    });\n    return str;\n}\n\n/**\n * Escapes a string to use as a RegExp.\n * @url https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping\n *\n * @param {string} str\n * @returns {string} escaped string to use as a RegExp\n */\nexport function escapeRegExp(str) {\n    return str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Intersperses ``separator`` in ``str`` at the positions indicated by\n * ``indices``.\n *\n * ``indices`` is an array of relative offsets (from the previous insertion\n * position, starting from the end of the string) at which to insert\n * ``separator``.\n *\n * There are two special values:\n *\n * ``-1``\n *   indicates the insertion should end now\n * ``0``\n *   indicates that the previous section pattern should be repeated (until all\n *   of ``str`` is consumed)\n *\n * @param {string} str\n * @param {number[]} indices\n * @param {string} separator\n * @returns {string}\n */\nexport function intersperse(str, indices, separator = \"\") {\n    separator = separator || \"\";\n    const result = [];\n    let last = str.length;\n    for (let i = 0; i < indices.length; ++i) {\n        let section = indices[i];\n        if (section === -1 || last <= 0) {\n            // Done with string, or -1 (stops formatting string)\n            break;\n        } else if (section === 0 && i === 0) {\n            // repeats previous section, which there is none => stop\n            break;\n        } else if (section === 0) {\n            // repeat previous section forever\n            //noinspection AssignmentToForLoopParameterJS\n            section = indices[--i];\n        }\n        result.push(str.substring(last - section, last));\n        last -= section;\n    }\n    const s = str.substring(0, last);\n    if (s) {\n        result.push(s);\n    }\n    return result.reverse().join(separator);\n}\n\n/**\n * Returns a string formatted using given values.\n * If the value is an object, its keys will replace `%(key)s` expressions.\n * If the values are a set of strings, they will replace `%s` expressions.\n * If no value is given, the string will not be formatted.\n *\n * @param {string} s\n * @param {any[]} values\n * @returns {string}\n */\nexport function sprintf(s, ...values) {\n    if (values.length === 1 && Object.prototype.toString.call(values[0]) === \"[object Object]\") {\n        const valuesDict = values[0];\n        s = s.replace(/%\\(([^)]+)\\)s/g, (match, value) => valuesDict[value]);\n    } else if (values.length > 0) {\n        s = s.replace(/%s/g, () => values.shift());\n    }\n    return s;\n}\n\n/**\n * Capitalizes a string: \"abc def\" => \"Abc def\"\n *\n * @param {string} s the input string\n * @returns {string}\n */\nexport function capitalize(s) {\n    return s ? s[0].toUpperCase() + s.slice(1) : \"\";\n}\n\n/* eslint-disable */\n// prettier-ignore\nconst diacriticsMap = {\n'\\u0041': 'A','\\u24B6': 'A','\\uFF21': 'A','\\u00C0': 'A','\\u00C1': 'A','\\u00C2': 'A','\\u1EA6': 'A','\\u1EA4': 'A','\\u1EAA': 'A','\\u1EA8': 'A',\n'\\u00C3': 'A','\\u0100': 'A','\\u0102': 'A','\\u1EB0': 'A','\\u1EAE': 'A','\\u1EB4': 'A','\\u1EB2': 'A','\\u0226': 'A','\\u01E0': 'A','\\u00C4': 'A',\n'\\u01DE': 'A','\\u1EA2': 'A','\\u00C5': 'A','\\u01FA': 'A','\\u01CD': 'A','\\u0200': 'A','\\u0202': 'A','\\u1EA0': 'A','\\u1EAC': 'A','\\u1EB6': 'A',\n'\\u1E00': 'A','\\u0104': 'A','\\u023A': 'A','\\u2C6F': 'A',\n\n'\\uA732': 'AA',\n'\\u00C6': 'AE','\\u01FC': 'AE','\\u01E2': 'AE',\n'\\uA734': 'AO',\n'\\uA736': 'AU',\n'\\uA738': 'AV','\\uA73A': 'AV',\n'\\uA73C': 'AY',\n'\\u0042': 'B','\\u24B7': 'B','\\uFF22': 'B','\\u1E02': 'B','\\u1E04': 'B','\\u1E06': 'B','\\u0243': 'B','\\u0182': 'B','\\u0181': 'B',\n\n'\\u0043': 'C','\\u24B8': 'C','\\uFF23': 'C','\\u0106': 'C','\\u0108': 'C','\\u010A': 'C','\\u010C': 'C','\\u00C7': 'C','\\u1E08': 'C','\\u0187': 'C',\n'\\u023B': 'C','\\uA73E': 'C',\n\n'\\u0044': 'D','\\u24B9': 'D','\\uFF24': 'D','\\u1E0A': 'D','\\u010E': 'D','\\u1E0C': 'D','\\u1E10': 'D','\\u1E12': 'D','\\u1E0E': 'D','\\u0110': 'D',\n'\\u018B': 'D','\\u018A': 'D','\\u0189': 'D','\\uA779': 'D',\n\n'\\u01F1': 'DZ','\\u01C4': 'DZ',\n'\\u01F2': 'Dz','\\u01C5': 'Dz',\n\n'\\u0045': 'E','\\u24BA': 'E','\\uFF25': 'E','\\u00C8': 'E','\\u00C9': 'E','\\u00CA': 'E','\\u1EC0': 'E','\\u1EBE': 'E','\\u1EC4': 'E','\\u1EC2': 'E',\n'\\u1EBC': 'E','\\u0112': 'E','\\u1E14': 'E','\\u1E16': 'E','\\u0114': 'E','\\u0116': 'E','\\u00CB': 'E','\\u1EBA': 'E','\\u011A': 'E','\\u0204': 'E',\n'\\u0206': 'E','\\u1EB8': 'E','\\u1EC6': 'E','\\u0228': 'E','\\u1E1C': 'E','\\u0118': 'E','\\u1E18': 'E','\\u1E1A': 'E','\\u0190': 'E','\\u018E': 'E',\n\n'\\u0046': 'F','\\u24BB': 'F','\\uFF26': 'F','\\u1E1E': 'F','\\u0191': 'F','\\uA77B': 'F',\n\n'\\u0047': 'G','\\u24BC': 'G','\\uFF27': 'G','\\u01F4': 'G','\\u011C': 'G','\\u1E20': 'G','\\u011E': 'G','\\u0120': 'G','\\u01E6': 'G','\\u0122': 'G',\n'\\u01E4': 'G','\\u0193': 'G','\\uA7A0': 'G','\\uA77D': 'G','\\uA77E': 'G',\n\n'\\u0048': 'H','\\u24BD': 'H','\\uFF28': 'H','\\u0124': 'H','\\u1E22': 'H','\\u1E26': 'H','\\u021E': 'H','\\u1E24': 'H','\\u1E28': 'H','\\u1E2A': 'H',\n'\\u0126': 'H','\\u2C67': 'H','\\u2C75': 'H','\\uA78D': 'H',\n\n'\\u0049': 'I','\\u24BE': 'I','\\uFF29': 'I','\\u00CC': 'I','\\u00CD': 'I','\\u00CE': 'I','\\u0128': 'I','\\u012A': 'I','\\u012C': 'I','\\u0130': 'I',\n'\\u00CF': 'I','\\u1E2E': 'I','\\u1EC8': 'I','\\u01CF': 'I','\\u0208': 'I','\\u020A': 'I','\\u1ECA': 'I','\\u012E': 'I','\\u1E2C': 'I','\\u0197': 'I',\n\n'\\u004A': 'J','\\u24BF': 'J','\\uFF2A': 'J','\\u0134': 'J','\\u0248': 'J',\n\n'\\u004B': 'K','\\u24C0': 'K','\\uFF2B': 'K','\\u1E30': 'K','\\u01E8': 'K','\\u1E32': 'K','\\u0136': 'K','\\u1E34': 'K','\\u0198': 'K','\\u2C69': 'K',\n'\\uA740': 'K','\\uA742': 'K','\\uA744': 'K','\\uA7A2': 'K',\n\n'\\u004C': 'L','\\u24C1': 'L','\\uFF2C': 'L','\\u013F': 'L','\\u0139': 'L','\\u013D': 'L','\\u1E36': 'L','\\u1E38': 'L','\\u013B': 'L','\\u1E3C': 'L',\n'\\u1E3A': 'L','\\u0141': 'L','\\u023D': 'L','\\u2C62': 'L','\\u2C60': 'L','\\uA748': 'L','\\uA746': 'L','\\uA780': 'L',\n\n'\\u01C7': 'LJ',\n'\\u01C8': 'Lj',\n'\\u004D': 'M','\\u24C2': 'M','\\uFF2D': 'M','\\u1E3E': 'M','\\u1E40': 'M','\\u1E42': 'M','\\u2C6E': 'M','\\u019C': 'M',\n\n'\\u004E': 'N','\\u24C3': 'N','\\uFF2E': 'N','\\u01F8': 'N','\\u0143': 'N','\\u00D1': 'N','\\u1E44': 'N','\\u0147': 'N','\\u1E46': 'N','\\u0145': 'N',\n'\\u1E4A': 'N','\\u1E48': 'N','\\u0220': 'N','\\u019D': 'N','\\uA790': 'N','\\uA7A4': 'N',\n\n'\\u01CA': 'NJ',\n'\\u01CB': 'Nj',\n\n'\\u004F': 'O','\\u24C4': 'O','\\uFF2F': 'O','\\u00D2': 'O','\\u00D3': 'O','\\u00D4': 'O','\\u1ED2': 'O','\\u1ED0': 'O','\\u1ED6': 'O','\\u1ED4': 'O',\n'\\u00D5': 'O','\\u1E4C': 'O','\\u022C': 'O','\\u1E4E': 'O','\\u014C': 'O','\\u1E50': 'O','\\u1E52': 'O','\\u014E': 'O','\\u022E': 'O','\\u0230': 'O',\n'\\u00D6': 'O','\\u022A': 'O','\\u1ECE': 'O','\\u0150': 'O','\\u01D1': 'O','\\u020C': 'O','\\u020E': 'O','\\u01A0': 'O','\\u1EDC': 'O','\\u1EDA': 'O',\n'\\u1EE0': 'O','\\u1EDE': 'O','\\u1EE2': 'O','\\u1ECC': 'O','\\u1ED8': 'O','\\u01EA': 'O','\\u01EC': 'O','\\u00D8': 'O','\\u01FE': 'O','\\u0186': 'O',\n'\\u019F': 'O','\\uA74A': 'O','\\uA74C': 'O',\n\n'\\u01A2': 'OI',\n'\\uA74E': 'OO',\n'\\u0222': 'OU',\n'\\u0050': 'P','\\u24C5': 'P','\\uFF30': 'P','\\u1E54': 'P','\\u1E56': 'P','\\u01A4': 'P','\\u2C63': 'P','\\uA750': 'P','\\uA752': 'P','\\uA754': 'P',\n'\\u0051': 'Q','\\u24C6': 'Q','\\uFF31': 'Q','\\uA756': 'Q','\\uA758': 'Q','\\u024A': 'Q',\n\n'\\u0052': 'R','\\u24C7': 'R','\\uFF32': 'R','\\u0154': 'R','\\u1E58': 'R','\\u0158': 'R','\\u0210': 'R','\\u0212': 'R','\\u1E5A': 'R','\\u1E5C': 'R',\n'\\u0156': 'R','\\u1E5E': 'R','\\u024C': 'R','\\u2C64': 'R','\\uA75A': 'R','\\uA7A6': 'R','\\uA782': 'R',\n\n'\\u0053': 'S','\\u24C8': 'S','\\uFF33': 'S','\\u1E9E': 'S','\\u015A': 'S','\\u1E64': 'S','\\u015C': 'S','\\u1E60': 'S','\\u0160': 'S','\\u1E66': 'S',\n'\\u1E62': 'S','\\u1E68': 'S','\\u0218': 'S','\\u015E': 'S','\\u2C7E': 'S','\\uA7A8': 'S','\\uA784': 'S',\n\n'\\u0054': 'T','\\u24C9': 'T','\\uFF34': 'T','\\u1E6A': 'T','\\u0164': 'T','\\u1E6C': 'T','\\u021A': 'T','\\u0162': 'T','\\u1E70': 'T','\\u1E6E': 'T',\n'\\u0166': 'T','\\u01AC': 'T','\\u01AE': 'T','\\u023E': 'T','\\uA786': 'T',\n\n'\\uA728': 'TZ',\n\n'\\u0055': 'U','\\u24CA': 'U','\\uFF35': 'U','\\u00D9': 'U','\\u00DA': 'U','\\u00DB': 'U','\\u0168': 'U','\\u1E78': 'U','\\u016A': 'U','\\u1E7A': 'U',\n'\\u016C': 'U','\\u00DC': 'U','\\u01DB': 'U','\\u01D7': 'U','\\u01D5': 'U','\\u01D9': 'U','\\u1EE6': 'U','\\u016E': 'U','\\u0170': 'U','\\u01D3': 'U',\n'\\u0214': 'U','\\u0216': 'U','\\u01AF': 'U','\\u1EEA': 'U','\\u1EE8': 'U','\\u1EEE': 'U','\\u1EEC': 'U','\\u1EF0': 'U','\\u1EE4': 'U','\\u1E72': 'U',\n'\\u0172': 'U','\\u1E76': 'U','\\u1E74': 'U','\\u0244': 'U',\n\n'\\u0056': 'V','\\u24CB': 'V','\\uFF36': 'V','\\u1E7C': 'V','\\u1E7E': 'V','\\u01B2': 'V','\\uA75E': 'V','\\u0245': 'V',\n'\\uA760': 'VY',\n'\\u0057': 'W','\\u24CC': 'W','\\uFF37': 'W','\\u1E80': 'W','\\u1E82': 'W','\\u0174': 'W','\\u1E86': 'W','\\u1E84': 'W','\\u1E88': 'W','\\u2C72': 'W',\n'\\u0058': 'X','\\u24CD': 'X','\\uFF38': 'X','\\u1E8A': 'X','\\u1E8C': 'X',\n\n'\\u0059': 'Y','\\u24CE': 'Y','\\uFF39': 'Y','\\u1EF2': 'Y','\\u00DD': 'Y','\\u0176': 'Y','\\u1EF8': 'Y','\\u0232': 'Y','\\u1E8E': 'Y','\\u0178': 'Y',\n'\\u1EF6': 'Y','\\u1EF4': 'Y','\\u01B3': 'Y','\\u024E': 'Y','\\u1EFE': 'Y',\n\n'\\u005A': 'Z','\\u24CF': 'Z','\\uFF3A': 'Z','\\u0179': 'Z','\\u1E90': 'Z','\\u017B': 'Z','\\u017D': 'Z','\\u1E92': 'Z','\\u1E94': 'Z','\\u01B5': 'Z',\n'\\u0224': 'Z','\\u2C7F': 'Z','\\u2C6B': 'Z','\\uA762': 'Z',\n\n'\\u0061': 'a','\\u24D0': 'a','\\uFF41': 'a','\\u1E9A': 'a','\\u00E0': 'a','\\u00E1': 'a','\\u00E2': 'a','\\u1EA7': 'a','\\u1EA5': 'a','\\u1EAB': 'a',\n'\\u1EA9': 'a','\\u00E3': 'a','\\u0101': 'a','\\u0103': 'a','\\u1EB1': 'a','\\u1EAF': 'a','\\u1EB5': 'a','\\u1EB3': 'a','\\u0227': 'a','\\u01E1': 'a',\n'\\u00E4': 'a','\\u01DF': 'a','\\u1EA3': 'a','\\u00E5': 'a','\\u01FB': 'a','\\u01CE': 'a','\\u0201': 'a','\\u0203': 'a','\\u1EA1': 'a','\\u1EAD': 'a',\n'\\u1EB7': 'a','\\u1E01': 'a','\\u0105': 'a','\\u2C65': 'a','\\u0250': 'a',\n\n'\\uA733': 'aa',\n'\\u00E6': 'ae','\\u01FD': 'ae','\\u01E3': 'ae',\n'\\uA735': 'ao',\n'\\uA737': 'au',\n'\\uA739': 'av','\\uA73B': 'av',\n'\\uA73D': 'ay',\n'\\u0062': 'b','\\u24D1': 'b','\\uFF42': 'b','\\u1E03': 'b','\\u1E05': 'b','\\u1E07': 'b','\\u0180': 'b','\\u0183': 'b','\\u0253': 'b',\n\n'\\u0063': 'c','\\u24D2': 'c','\\uFF43': 'c','\\u0107': 'c','\\u0109': 'c','\\u010B': 'c','\\u010D': 'c','\\u00E7': 'c','\\u1E09': 'c','\\u0188': 'c',\n'\\u023C': 'c','\\uA73F': 'c','\\u2184': 'c',\n\n'\\u0064': 'd','\\u24D3': 'd','\\uFF44': 'd','\\u1E0B': 'd','\\u010F': 'd','\\u1E0D': 'd','\\u1E11': 'd','\\u1E13': 'd','\\u1E0F': 'd','\\u0111': 'd',\n'\\u018C': 'd','\\u0256': 'd','\\u0257': 'd','\\uA77A': 'd',\n\n'\\u01F3': 'dz','\\u01C6': 'dz',\n\n'\\u0065': 'e','\\u24D4': 'e','\\uFF45': 'e','\\u00E8': 'e','\\u00E9': 'e','\\u00EA': 'e','\\u1EC1': 'e','\\u1EBF': 'e','\\u1EC5': 'e','\\u1EC3': 'e',\n'\\u1EBD': 'e','\\u0113': 'e','\\u1E15': 'e','\\u1E17': 'e','\\u0115': 'e','\\u0117': 'e','\\u00EB': 'e','\\u1EBB': 'e','\\u011B': 'e','\\u0205': 'e',\n'\\u0207': 'e','\\u1EB9': 'e','\\u1EC7': 'e','\\u0229': 'e','\\u1E1D': 'e','\\u0119': 'e','\\u1E19': 'e','\\u1E1B': 'e','\\u0247': 'e','\\u025B': 'e',\n'\\u01DD': 'e',\n\n'\\u0066': 'f','\\u24D5': 'f','\\uFF46': 'f','\\u1E1F': 'f','\\u0192': 'f','\\uA77C': 'f',\n\n'\\u0067': 'g','\\u24D6': 'g','\\uFF47': 'g','\\u01F5': 'g','\\u011D': 'g','\\u1E21': 'g','\\u011F': 'g','\\u0121': 'g','\\u01E7': 'g','\\u0123': 'g',\n'\\u01E5': 'g','\\u0260': 'g','\\uA7A1': 'g','\\u1D79': 'g','\\uA77F': 'g',\n\n'\\u0068': 'h','\\u24D7': 'h','\\uFF48': 'h','\\u0125': 'h','\\u1E23': 'h','\\u1E27': 'h','\\u021F': 'h','\\u1E25': 'h','\\u1E29': 'h','\\u1E2B': 'h',\n'\\u1E96': 'h','\\u0127': 'h','\\u2C68': 'h','\\u2C76': 'h','\\u0265': 'h',\n\n'\\u0195': 'hv',\n\n'\\u0069': 'i','\\u24D8': 'i','\\uFF49': 'i','\\u00EC': 'i','\\u00ED': 'i','\\u00EE': 'i','\\u0129': 'i','\\u012B': 'i','\\u012D': 'i','\\u00EF': 'i',\n'\\u1E2F': 'i','\\u1EC9': 'i','\\u01D0': 'i','\\u0209': 'i','\\u020B': 'i','\\u1ECB': 'i','\\u012F': 'i','\\u1E2D': 'i','\\u0268': 'i','\\u0131': 'i',\n\n'\\u006A': 'j','\\u24D9': 'j','\\uFF4A': 'j','\\u0135': 'j','\\u01F0': 'j','\\u0249': 'j',\n\n'\\u006B': 'k','\\u24DA': 'k','\\uFF4B': 'k','\\u1E31': 'k','\\u01E9': 'k','\\u1E33': 'k','\\u0137': 'k','\\u1E35': 'k','\\u0199': 'k','\\u2C6A': 'k',\n'\\uA741': 'k','\\uA743': 'k','\\uA745': 'k','\\uA7A3': 'k',\n\n'\\u006C': 'l','\\u24DB': 'l','\\uFF4C': 'l','\\u0140': 'l','\\u013A': 'l','\\u013E': 'l','\\u1E37': 'l','\\u1E39': 'l','\\u013C': 'l','\\u1E3D': 'l',\n'\\u1E3B': 'l','\\u017F': 'l','\\u0142': 'l','\\u019A': 'l','\\u026B': 'l','\\u2C61': 'l','\\uA749': 'l','\\uA781': 'l','\\uA747': 'l',\n\n'\\u01C9': 'lj',\n'\\u006D': 'm','\\u24DC': 'm','\\uFF4D': 'm','\\u1E3F': 'm','\\u1E41': 'm','\\u1E43': 'm','\\u0271': 'm','\\u026F': 'm',\n\n'\\u006E': 'n','\\u24DD': 'n','\\uFF4E': 'n','\\u01F9': 'n','\\u0144': 'n','\\u00F1': 'n','\\u1E45': 'n','\\u0148': 'n','\\u1E47': 'n','\\u0146': 'n',\n'\\u1E4B': 'n','\\u1E49': 'n','\\u019E': 'n','\\u0272': 'n','\\u0149': 'n','\\uA791': 'n','\\uA7A5': 'n',\n\n'\\u01CC': 'nj',\n\n'\\u006F': 'o','\\u24DE': 'o','\\uFF4F': 'o','\\u00F2': 'o','\\u00F3': 'o','\\u00F4': 'o','\\u1ED3': 'o','\\u1ED1': 'o','\\u1ED7': 'o','\\u1ED5': 'o',\n'\\u00F5': 'o','\\u1E4D': 'o','\\u022D': 'o','\\u1E4F': 'o','\\u014D': 'o','\\u1E51': 'o','\\u1E53': 'o','\\u014F': 'o','\\u022F': 'o','\\u0231': 'o',\n'\\u00F6': 'o','\\u022B': 'o','\\u1ECF': 'o','\\u0151': 'o','\\u01D2': 'o','\\u020D': 'o','\\u020F': 'o','\\u01A1': 'o','\\u1EDD': 'o','\\u1EDB': 'o',\n'\\u1EE1': 'o','\\u1EDF': 'o','\\u1EE3': 'o','\\u1ECD': 'o','\\u1ED9': 'o','\\u01EB': 'o','\\u01ED': 'o','\\u00F8': 'o','\\u01FF': 'o','\\u0254': 'o',\n'\\uA74B': 'o','\\uA74D': 'o','\\u0275': 'o',\n\n'\\u01A3': 'oi',\n'\\u0223': 'ou',\n'\\uA74F': 'oo',\n'\\u0070': 'p','\\u24DF': 'p','\\uFF50': 'p','\\u1E55': 'p','\\u1E57': 'p','\\u01A5': 'p','\\u1D7D': 'p','\\uA751': 'p','\\uA753': 'p','\\uA755': 'p',\n'\\u0071': 'q','\\u24E0': 'q','\\uFF51': 'q','\\u024B': 'q','\\uA757': 'q','\\uA759': 'q',\n\n'\\u0072': 'r','\\u24E1': 'r','\\uFF52': 'r','\\u0155': 'r','\\u1E59': 'r','\\u0159': 'r','\\u0211': 'r','\\u0213': 'r','\\u1E5B': 'r','\\u1E5D': 'r',\n'\\u0157': 'r','\\u1E5F': 'r','\\u024D': 'r','\\u027D': 'r','\\uA75B': 'r','\\uA7A7': 'r','\\uA783': 'r',\n\n'\\u0073': 's','\\u24E2': 's','\\uFF53': 's','\\u00DF': 's','\\u015B': 's','\\u1E65': 's','\\u015D': 's','\\u1E61': 's','\\u0161': 's','\\u1E67': 's',\n'\\u1E63': 's','\\u1E69': 's','\\u0219': 's','\\u015F': 's','\\u023F': 's','\\uA7A9': 's','\\uA785': 's','\\u1E9B': 's',\n\n'\\u0074': 't','\\u24E3': 't','\\uFF54': 't','\\u1E6B': 't','\\u1E97': 't','\\u0165': 't','\\u1E6D': 't','\\u021B': 't','\\u0163': 't','\\u1E71': 't',\n'\\u1E6F': 't','\\u0167': 't','\\u01AD': 't','\\u0288': 't','\\u2C66': 't','\\uA787': 't',\n\n'\\uA729': 'tz',\n\n'\\u0075': 'u','\\u24E4': 'u','\\uFF55': 'u','\\u00F9': 'u','\\u00FA': 'u','\\u00FB': 'u','\\u0169': 'u','\\u1E79': 'u','\\u016B': 'u','\\u1E7B': 'u',\n'\\u016D': 'u','\\u00FC': 'u','\\u01DC': 'u','\\u01D8': 'u','\\u01D6': 'u','\\u01DA': 'u','\\u1EE7': 'u','\\u016F': 'u','\\u0171': 'u','\\u01D4': 'u',\n'\\u0215': 'u','\\u0217': 'u','\\u01B0': 'u','\\u1EEB': 'u','\\u1EE9': 'u','\\u1EEF': 'u','\\u1EED': 'u','\\u1EF1': 'u','\\u1EE5': 'u','\\u1E73': 'u',\n'\\u0173': 'u','\\u1E77': 'u','\\u1E75': 'u','\\u0289': 'u',\n\n'\\u0076': 'v','\\u24E5': 'v','\\uFF56': 'v','\\u1E7D': 'v','\\u1E7F': 'v','\\u028B': 'v','\\uA75F': 'v','\\u028C': 'v',\n'\\uA761': 'vy',\n'\\u0077': 'w','\\u24E6': 'w','\\uFF57': 'w','\\u1E81': 'w','\\u1E83': 'w','\\u0175': 'w','\\u1E87': 'w','\\u1E85': 'w','\\u1E98': 'w','\\u1E89': 'w',\n'\\u2C73': 'w',\n'\\u0078': 'x','\\u24E7': 'x','\\uFF58': 'x','\\u1E8B': 'x','\\u1E8D': 'x',\n\n'\\u0079': 'y','\\u24E8': 'y','\\uFF59': 'y','\\u1EF3': 'y','\\u00FD': 'y','\\u0177': 'y','\\u1EF9': 'y','\\u0233': 'y','\\u1E8F': 'y','\\u00FF': 'y',\n'\\u1EF7': 'y','\\u1E99': 'y','\\u1EF5': 'y','\\u01B4': 'y','\\u024F': 'y','\\u1EFF': 'y',\n\n'\\u007A': 'z','\\u24E9': 'z','\\uFF5A': 'z','\\u017A': 'z','\\u1E91': 'z','\\u017C': 'z','\\u017E': 'z','\\u1E93': 'z','\\u1E95': 'z','\\u01B6': 'z',\n'\\u0225': 'z','\\u0240': 'z','\\u2C6C': 'z','\\uA763': 'z',\n};\n\n/**\n * Replace diacritics character with ASCII character\n *\n * @param {string} str diacritics string\n * @param {boolean} caseSensitive\n * @returns {string} ASCII string\n */\nexport function unaccent(str, caseSensitive) {\n    str = str.replace(/[^\\u0000-\\u007E]/g, function (accented) {\n        return diacriticsMap[accented] || accented;\n    });\n    return caseSensitive ? str : str.toLowerCase();\n}\n\n/**\n * @param {string} value\n * @returns boolean\n */\nexport function isEmail(value) {\n    // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript\n    const re = /^(([^<>()\\[\\]\\.,;:\\s@\\\"]+(\\.[^<>()\\[\\]\\.,;:\\s@\\\"]+)*)|(\\\".+\\\"))@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i;\n    return re.test(value);\n}\n\n/**\n * Return true if the string is composed of only digits\n *\n * @param {string} value\n * @returns boolean\n */\n\nexport function isNumeric(value) {\n    return Boolean(value?.match(/^\\d+$/));\n}\n\n/**\n * Parse the string to check if the value is true or false\n * If the string is empty, 0, False or false it's considered as false\n * The rest is considered as true\n *\n * @param {string} str\n * @param {boolean} [trueIfEmpty=false]\n * @returns {boolean}\n */\nexport function exprToBoolean(str, trueIfEmpty = false) {\n    return str ? !/^false|0$/i.test(str) : trueIfEmpty;\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { onWillUnmount, useComponent } from \"@odoo/owl\";\n\n/**\n * Creates a batched version of a callback so that all calls to it in the same\n * time frame will only call the original callback once.\n * @param callback the callback to batch\n * @param synchronize this function decides the granularity of the batch (a microtick by default)\n * @returns a batched version of the original callback\n */\nexport function batched(callback, synchronize = () => Promise.resolve()) {\n    let scheduled = false;\n    return async (...args) => {\n        if (!scheduled) {\n            scheduled = true;\n            await synchronize();\n            scheduled = false;\n            callback(...args);\n        }\n    };\n}\n\n/**\n * Creates and returns a new debounced version of the passed function (func)\n * which will postpone its execution until after 'delay' milliseconds\n * have elapsed since the last time it was invoked. The debounced function\n * will return a Promise that will be resolved when the function (func)\n * has been fully executed.\n *\n * If both `options.trailing` and `options.leading` are true, the function\n * will only be invoked at the trailing edge if the debounced function was\n * called at least once more during the wait time.\n *\n * @template {Function} T the return type of the original function\n * @param {T} func the function to debounce\n * @param {number | \"animationFrame\"} delay how long should elapse before the function\n *      is called. If 'animationFrame' is given instead of a number, 'requestAnimationFrame'\n *      will be used instead of 'setTimeout'.\n * @param {boolean} [options] if true, equivalent to exclusive leading. If false, equivalent to exclusive trailing.\n * @param {object} [options]\n * @param {boolean} [options.leading=false] whether the function should be invoked at the leading edge of the timeout\n * @param {boolean} [options.trailing=true] whether the function should be invoked at the trailing edge of the timeout\n * @returns {T & { cancel: () => void }} the debounced function\n */\nexport function debounce(func, delay, options) {\n    let handle;\n    const funcName = func.name ? func.name + \" (debounce)\" : \"debounce\";\n    const useAnimationFrame = delay === \"animationFrame\";\n    const setFnName = useAnimationFrame ? \"requestAnimationFrame\" : \"setTimeout\";\n    const clearFnName = useAnimationFrame ? \"cancelAnimationFrame\" : \"clearTimeout\";\n    let lastArgs;\n    let leading = false;\n    let trailing = true;\n    if (typeof options === \"boolean\") {\n        leading = options;\n        trailing = !options;\n    } else if (options) {\n        leading = options.leading ?? leading;\n        trailing = options.trailing ?? trailing;\n    }\n\n    return Object.assign(\n        {\n            /** @type {any} */\n            [funcName](...args) {\n                return new Promise((resolve) => {\n                    if (leading && !handle) {\n                        Promise.resolve(func.apply(this, args)).then(resolve);\n                    } else {\n                        lastArgs = args;\n                    }\n                    browser[clearFnName](handle);\n                    handle = browser[setFnName](() => {\n                        handle = null;\n                        if (trailing && lastArgs) {\n                            Promise.resolve(func.apply(this, lastArgs)).then(resolve);\n                            lastArgs = null;\n                        }\n                    }, delay);\n                });\n            },\n        }[funcName],\n        {\n            cancel(execNow = false) {\n                browser[clearFnName](handle);\n                if (execNow && lastArgs) {\n                    func.apply(this, lastArgs);\n                }\n            },\n        }\n    );\n}\n\n/**\n * Function that calls recursively a request to an animation frame.\n * Useful to call a function repetitively, until asked to stop, that needs constant rerendering.\n * The provided callback gets as argument the time the last frame took.\n * @param {(deltaTime: number) => void} callback\n * @returns {() => void} stop function\n */\nexport function setRecurringAnimationFrame(callback) {\n    const handler = (timestamp) => {\n        callback(timestamp - lastTimestamp);\n        lastTimestamp = timestamp;\n        handle = browser.requestAnimationFrame(handler);\n    };\n\n    const stop = () => {\n        browser.cancelAnimationFrame(handle);\n    };\n\n    let lastTimestamp = browser.performance.now();\n    let handle = browser.requestAnimationFrame(handler);\n\n    return stop;\n}\n\n/**\n * Creates a version of the function where only the last call between two\n * animation frames is executed before the browser's next repaint. This\n * effectively throttles the function to the display's refresh rate.\n * Note that the throttled function can be any callback. It is not\n * specifically an event handler, no assumption is made about its\n * signature.\n * NB: The first call is always called immediately (leading edge).\n *\n * @template {Function} T\n * @param {T} func the function to throttle\n * @returns {T & { cancel: () => void }} the throttled function\n */\nexport function throttleForAnimation(func) {\n    let handle = null;\n    const calls = new Set();\n    const funcName = func.name ? `${func.name} (throttleForAnimation)` : \"throttleForAnimation\";\n    const pending = () => {\n        if (calls.size) {\n            handle = browser.requestAnimationFrame(pending);\n            const { args, resolve } = [...calls].pop();\n            calls.clear();\n            Promise.resolve(func.apply(this, args)).then(resolve);\n        } else {\n            handle = null;\n        }\n    };\n    return Object.assign(\n        {\n            /** @type {any} */\n            [funcName](...args) {\n                return new Promise((resolve) => {\n                    const isNew = handle === null;\n                    if (isNew) {\n                        handle = browser.requestAnimationFrame(pending);\n                        Promise.resolve(func.apply(this, args)).then(resolve);\n                    } else {\n                        calls.add({ args, resolve });\n                    }\n                });\n            },\n        }[funcName],\n        {\n            cancel() {\n                browser.cancelAnimationFrame(handle);\n                calls.clear();\n                handle = null;\n            },\n        }\n    );\n}\n\n// ----------------------------------- HOOKS -----------------------------------\n\n/**\n * Hook that returns a debounced version of the given function, and cancels\n * the potential pending execution on willUnmount.\n * @see debounce\n * @template {Function} T\n * @param {T} callback\n * @param {number | \"animationFrame\"} delay\n * @param {Object} [options]\n * @param {string} [options.execBeforeUnmount=false] executes the callback if the debounced function\n *      has been called and not resolved before destroying the component.\n * @param {boolean} [options.immediate=false] whether the function should be called on\n *      the leading edge instead of the trailing edge.\n * @returns {T & { cancel: () => void }}\n */\nexport function useDebounced(\n    callback,\n    delay,\n    { execBeforeUnmount = false, immediate = false } = {}\n) {\n    const component = useComponent();\n    const debounced = debounce(callback.bind(component), delay, immediate);\n    onWillUnmount(() => debounced.cancel(execBeforeUnmount));\n    return debounced;\n}\n\n/**\n * Hook that returns a throttled for animation version of the given function,\n * and cancels the potential pending execution on willUnmount.\n * @see throttleForAnimation\n * @template {Function} T\n * @param {T} func the function to throttle\n * @returns {T & { cancel: () => void }} the throttled function\n */\nexport function useThrottleForAnimation(func) {\n    const component = useComponent();\n    const throttledForAnimation = throttleForAnimation(func.bind(component));\n    onWillUnmount(() => throttledForAnimation.cancel());\n    return throttledForAnimation;\n}\n", "import { session } from \"@web/session\";\nimport { browser } from \"../browser/browser\";\nimport { shallowEqual } from \"@web/core/utils/objects\";\nconst { DateTime } = luxon;\n\nexport class RedirectionError extends Error {}\n\n/**\n * Transforms a key value mapping to a string formatted as url hash, e.g.\n * {a: \"x\", b: 2} -> \"a=x&b=2\"\n *\n * @param {Object} obj\n * @returns {string}\n */\nexport function objectToUrlEncodedString(obj) {\n    return Object.entries(obj)\n        .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v || \"\")}`)\n        .join(\"&\");\n}\n\n/**\n * Gets the origin url of the page, or cleans a given one\n *\n * @param {string} [origin]: a given origin url\n * @return {string} a cleaned origin url\n */\nexport function getOrigin(origin) {\n    if (origin) {\n        // remove trailing slashes\n        origin = origin.replace(/\\/+$/, \"\");\n    } else {\n        const { host, protocol } = browser.location;\n        origin = `${protocol}//${host}`;\n    }\n    return origin;\n}\n\n/**\n * @param {string} route: the relative route, or absolute in the case of cors urls\n * @param {object} [queryParams]: parameters to be appended as the url's queryString\n * @param {object} [options]\n * @param {string} [options.origin]: a precomputed origin\n */\nexport function url(route, queryParams, options = {}) {\n    const origin = getOrigin(options.origin ?? session.origin);\n    if (!route) {\n        return origin;\n    }\n\n    let queryString = objectToUrlEncodedString(queryParams || {});\n    queryString = queryString.length > 0 ? `?${queryString}` : queryString;\n\n    // Compare the wanted url against the current origin\n    let prefix = [\"http://\", \"https://\", \"//\"].some(\n        (el) => route.length >= el.length && route.slice(0, el.length) === el\n    );\n    prefix = prefix ? \"\" : origin;\n    return `${prefix}${route}${queryString}`;\n}\n\n/**\n * @param {string} model\n * @param {number} id\n * @param {string} field\n * @param {Object} [options]\n * @param {string} [options.filename]\n * @param {number} [options.height]\n * @param {string|import('luxon').DateTime} [options.unique]\n * @param {number} [options.width]\n */\nexport function imageUrl(model, id, field, { filename, height, unique, width } = {}) {\n    let route = `/web/image/${model}/${id}/${field}`;\n    if (width && height) {\n        route = `${route}/${width}x${height}`;\n    }\n    if (filename) {\n        route = `${route}/${filename}`;\n    }\n    const urlParams = {};\n    if (unique) {\n        if (unique instanceof DateTime) {\n            urlParams.unique = unique.ts;\n        } else {\n            const dateTimeFromUnique = DateTime.fromSQL(unique);\n            if (dateTimeFromUnique.isValid) {\n                urlParams.unique = dateTimeFromUnique.ts;\n            } else if (typeof unique === \"string\" && unique.length > 0) {\n                urlParams.unique = unique;\n            }\n        }\n    }\n    return url(route, urlParams);\n}\n\n/**\n * Gets dataURL (base64 data) from the given file or blob.\n * Technically wraps FileReader.readAsDataURL in Promise.\n *\n * @param {Blob | File} file\n * @returns {Promise} resolved with the dataURL, or rejected if the file is\n *  empty or if an error occurs.\n */\nexport function getDataURLFromFile(file) {\n    if (!file) {\n        return Promise.reject();\n    }\n    return new Promise((resolve, reject) => {\n        const reader = new FileReader();\n        reader.addEventListener(\"load\", () => {\n            // Handle Chrome bug that creates invalid data URLs for empty files\n            if (reader.result === \"data:\") {\n                resolve(`data:${file.type};base64,`);\n            } else {\n                resolve(reader.result);\n            }\n        });\n        reader.addEventListener(\"abort\", reject);\n        reader.addEventListener(\"error\", reject);\n        reader.readAsDataURL(file);\n    });\n}\n\n/**\n * Safely redirects to the given url within the same origin.\n *\n * @param {string} url\n * @throws {RedirectionError} if the given url has a different origin\n */\nexport function redirect(url) {\n    const { origin, pathname } = browser.location;\n    const _url = new URL(url, `${origin}${pathname}`);\n    if (_url.origin !== origin) {\n        throw new RedirectionError(\"Can't redirect to another origin\");\n    }\n    browser.location.assign(_url.href);\n}\n\n/**\n * This function compares two URLs. It doesn't care about the order of the search parameters.\n *\n * @param {string} _url1\n * @param {string} _url2\n * @returns {boolean} true if the urls are identical, false otherwise\n */\nexport function compareUrls(_url1, _url2) {\n    const url1 = new URL(_url1);\n    const url2 = new URL(_url2);\n    return (\n        url1.origin === url2.origin &&\n        url1.pathname === url2.pathname &&\n        shallowEqual(\n            Object.fromEntries(url1.searchParams),\n            Object.fromEntries(url2.searchParams)\n        ) &&\n        url1.hash === url2.hash\n    );\n}\n", "import { isIterable } from \"./arrays\";\n\n/**\n * XML document to create new elements from. The fact that this is a \"text/xml\"\n * document ensures that tagNames and attribute names are case sensitive.\n */\nconst serializer = new XMLSerializer();\nconst parser = new DOMParser();\nconst xmlDocument = parser.parseFromString(\"<templates/>\", \"text/xml\");\n\nfunction hasParsingError(parsedDocument) {\n    return parsedDocument.getElementsByTagName(\"parsererror\").length > 0;\n}\n\n/**\n * @param {string} str\n * @returns {Element}\n */\nexport function parseXML(str) {\n    const xml = parser.parseFromString(str, \"text/xml\");\n    if (hasParsingError(xml)) {\n        throw new Error(\n            `An error occured while parsing ${str}: ${xml.getElementsByTagName(\"parsererror\")}`\n        );\n    }\n    return xml.documentElement;\n}\n\n/**\n * @param {Element} xml\n * @returns {string}\n */\nexport function serializeXML(xml) {\n    return serializer.serializeToString(xml);\n}\n\n/**\n * @param {Element | string} xml\n * @param {(el: Element, visitChildren: () => any) => any} callback\n */\nexport function visitXML(xml, callback) {\n    const visit = (el) => {\n        if (el) {\n            let didVisitChildren = false;\n            const visitChildren = () => {\n                for (const child of el.children) {\n                    visit(child);\n                }\n                didVisitChildren = true;\n            };\n            const shouldVisitChildren = callback(el, visitChildren);\n            if (shouldVisitChildren !== false && !didVisitChildren) {\n                visitChildren();\n            }\n        }\n    };\n    const xmlDoc = typeof xml === \"string\" ? parseXML(xml) : xml;\n    visit(xmlDoc);\n}\n\n/**\n * @param {Element} parent\n * @param {Node | Node[] | void} node\n */\nexport function append(parent, node) {\n    const nodes = Array.isArray(node) ? node : [node];\n    parent.append(...nodes.filter(Boolean));\n    return parent;\n}\n\n/**\n * Combines the existing value of a node attribute with new given parts. The glue\n * is the string used to join the parts.\n *\n * @param {Element} el\n * @param {string} attr\n * @param {string | string[]} parts\n * @param {string} [glue=\" \"]\n */\nexport function combineAttributes(el, attr, parts, glue = \" \") {\n    const allValues = [];\n    if (el.hasAttribute(attr)) {\n        allValues.push(el.getAttribute(attr));\n    }\n    parts = Array.isArray(parts) ? parts : [parts];\n    parts = parts.filter((part) => !!part);\n    allValues.push(...parts);\n    el.setAttribute(attr, allValues.join(glue));\n}\n\n/**\n * XML equivalent of `document.createElement`.\n *\n * @param {string} tagName\n * @param {...(Iterable<Element> | Record<string, string>)} args\n * @returns {Element}\n */\nexport function createElement(tagName, ...args) {\n    const el = xmlDocument.createElement(tagName);\n    for (const arg of args) {\n        if (!arg) {\n            continue;\n        }\n        if (isIterable(arg)) {\n            // Children list\n            el.append(...arg);\n        } else if (typeof arg === \"object\") {\n            // Attributes\n            for (const name in arg) {\n                el.setAttribute(name, arg[name]);\n            }\n        }\n    }\n    return el;\n}\n\n/**\n * XML equivalent of `document.createTextNode`.\n *\n * @param {string} data\n * @returns {Text}\n */\nexport function createTextNode(data) {\n    return xmlDocument.createTextNode(data);\n}\n\n/**\n * Removes the given attributes on the given element and returns them as a dictionnary.\n * @param {Element} el\n * @param {string[]} attributes\n * @returns {Record<string, string>}\n */\nexport function extractAttributes(el, attributes) {\n    const attrs = Object.create(null);\n    for (const attr of attributes) {\n        attrs[attr] = el.getAttribute(attr) || \"\";\n        el.removeAttribute(attr);\n    }\n    return attrs;\n}\n\n/**\n * @param {Node} [node]\n * @param {boolean} [lower=false]\n * @returns {string}\n */\nexport function getTag(node, lower = false) {\n    const tag = (node && node.nodeName) || \"\";\n    return lower ? tag.toLowerCase() : tag;\n}\n\n/**\n * @param {Node} node\n * @param {Object} attributes\n */\nexport function setAttributes(node, attributes) {\n    for (const [name, value] of Object.entries(attributes)) {\n        node.setAttribute(name, value);\n    }\n}\n", "import { useComponent, useEffect, useExternalListener } from \"@odoo/owl\";\nimport { pick, shallowEqual } from \"@web/core/utils/objects\";\nimport { useThrottleForAnimation } from \"@web/core/utils/timing\";\n\n/**\n * @template T\n * @typedef VirtualGridParams\n * @property {ReturnType<typeof import(\"@odoo/owl\").useRef>} scrollableRef\n *  a ref to the scrollable element\n * @property {ScrollPosition} [initialScroll={ left: 0, top: 0 }]\n *  the initial scroll position of the scrollable element\n * @property {(changed: Partial<VirtualGridIndexes>) => void} [onChange=() => this.render()]\n *  a callback called when the visible items change, i.e. when on scroll or resize.\n *  the default implementation is to re-render the component.\n * @property {number} [bufferCoef=1]\n *  the coefficient to calculate the buffer size around the visible area.\n *  The buffer size is equal to bufferCoef * windowSize.\n *  The default value is 1: it means that the buffer size takes one more window size on each side.\n *  So the whole area that will be rendered is 3 times the window size.\n *  If you use each direction, it could be up to 9 times the window size (3x3).\n *  Consider lowering this value if you have a costful rendering.\n *  A value of 0 means no buffer.\n */\n\n/**\n * @typedef VirtualGridIndexes\n * @property {[number, number] | undefined} columnsIndexes\n * @property {[number, number] | undefined} rowsIndexes\n */\n\n/**\n * @typedef VirtualGridSetters\n * @property {(widths: number[]) => void} setColumnsWidths\n *  Use it to set the width of each column.\n *  Indexes should match the indexes of the columns.\n * @property {(heights: number[]) => void} setRowsHeights\n *  Use it to set the height of each row.\n *  Indexes should match the indexes of the rows.\n */\n\n/**\n * @typedef ScrollPosition\n * @property {number} left\n * @property {number} top\n */\n\nconst BUFFER_COEFFICIENT = 1;\n\n/**\n * @typedef GetIndexesParams\n * @property {number[]} sizes contains the sizes of the items. Each size is the sum of the sizes of the previous items and the size of the current item.\n * @property {number} start it is the start position of the visible area, here it is the scroll position.\n * @property {number} span it is the size of the visible area, here it is the window size.\n * @property {number} [prevStartIndex] the previous start index, it is used to optimize the calculation.\n * @property {number} [bufferCoef=BUFFER_COEFFICIENT] the coefficient to calculate the buffer size.\n */\n\n/**\n * This function calculates the indexes of the visible items in a virtual list.\n *\n * @param {GetIndexesParams} param0\n * @returns {[number, number] | undefined} the indexes of the visible items with a surrounding buffer of totalSize on each side.\n */\nfunction getIndexes({ sizes, start, span, prevStartIndex, bufferCoef = BUFFER_COEFFICIENT }) {\n    if (!sizes || !sizes.length) {\n        return [];\n    }\n    if (sizes.at(-1) < span) {\n        // all items could be displayed\n        return [0, sizes.length - 1];\n    }\n    const bufferSize = Math.round(span * bufferCoef);\n    const bufferStart = start - bufferSize;\n    const bufferEnd = start + span + bufferSize;\n\n    let startIndex = prevStartIndex ?? 0;\n    // we search the first index such that sizes[index] > bufferStart\n    while (startIndex > 0 && sizes[startIndex] > bufferStart) {\n        startIndex--;\n    }\n    while (startIndex < sizes.length - 1 && sizes[startIndex] <= bufferStart) {\n        startIndex++;\n    }\n\n    let endIndex = startIndex;\n    // we search the last index such that (sizes[index - 1] ?? 0) < bufferEnd\n    while (endIndex < sizes.length - 1 && (sizes[endIndex - 1] ?? 0) < bufferEnd) {\n        endIndex++;\n    }\n    while (endIndex > startIndex && (sizes[endIndex - 1] ?? 0) >= bufferEnd) {\n        endIndex--;\n    }\n    return [startIndex, endIndex];\n}\n\n/**\n * Calculates the displayed items in a virtual grid.\n *\n * Requirements:\n *  - the scrollable area has a fixed height and width.\n *  - the items are rendered with a proper offset inside the scrollable area.\n *    This can be achieved e.g. with a css grid or an absolute positioning.\n *\n * @template T\n * @param {VirtualGridParams<T>} params\n * @returns {VirtualGridIndexes & VirtualGridSetters}\n */\nexport function useVirtualGrid({ scrollableRef, initialScroll, onChange, bufferCoef }) {\n    const comp = useComponent();\n    onChange ||= () => comp.render();\n\n    const current = { scroll: { left: 0, top: 0, ...initialScroll } };\n    const computeColumnsIndexes = () => {\n        return getIndexes({\n            sizes: current.summedColumnsWidths,\n            start: Math.abs(current.scroll.left),\n            span: window.innerWidth,\n            prevStartIndex: current.columnsIndexes?.[0],\n            bufferCoef,\n        });\n    };\n    const computeRowsIndexes = () => {\n        return getIndexes({\n            sizes: current.summedRowsHeights,\n            start: current.scroll.top,\n            span: window.innerHeight,\n            prevStartIndex: current.rowsIndexes?.[0],\n            bufferCoef,\n        });\n    };\n    const throttledCompute = useThrottleForAnimation(() => {\n        const changed = [];\n        const columnsVisibleIndexes = computeColumnsIndexes();\n        if (!shallowEqual(columnsVisibleIndexes, current.columnsIndexes)) {\n            current.columnsIndexes = columnsVisibleIndexes;\n            changed.push(\"columnsIndexes\");\n        }\n        const rowsVisibleIndexes = computeRowsIndexes();\n        if (!shallowEqual(rowsVisibleIndexes, current.rowsIndexes)) {\n            current.rowsIndexes = rowsVisibleIndexes;\n            changed.push(\"rowsIndexes\");\n        }\n        if (changed.length) {\n            onChange(pick(current, ...changed));\n        }\n    });\n    const scrollListener = (/** @type {Event & { target: Element }} */ ev) => {\n        current.scroll.left = ev.target.scrollLeft;\n        current.scroll.top = ev.target.scrollTop;\n        throttledCompute();\n    };\n    useEffect(\n        (el) => {\n            el?.addEventListener(\"scroll\", scrollListener);\n            return () => el?.removeEventListener(\"scroll\", scrollListener);\n        },\n        () => [scrollableRef.el]\n    );\n    useExternalListener(window, \"resize\", () => throttledCompute());\n    return {\n        get columnsIndexes() {\n            return current.columnsIndexes;\n        },\n        get rowsIndexes() {\n            return current.rowsIndexes;\n        },\n        setColumnsWidths(widths) {\n            let acc = 0;\n            current.summedColumnsWidths = widths.map((w) => (acc += w));\n            delete current.columnsIndexes;\n            current.columnsIndexes = computeColumnsIndexes();\n        },\n        setRowsHeights(heights) {\n            let acc = 0;\n            current.summedRowsHeights = heights.map((h) => (acc += h));\n            delete current.rowsIndexes;\n            current.rowsIndexes = computeRowsIndexes();\n        },\n    };\n}\n", "import { isMacOS } from \"@web/core/browser/feature_detection\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { capitalize } from \"@web/core/utils/strings\";\nimport { getVisibleElements } from \"@web/core/utils/ui\";\nimport { DefaultCommandItem } from \"./command_palette\";\n\nimport { Component } from \"@odoo/owl\";\n\nconst commandSetupRegistry = registry.category(\"command_setup\");\ncommandSetupRegistry.add(\"default\", {\n    emptyMessage: _t(\"No command found\"),\n    placeholder: _t(\"Search for a command...\"),\n});\n\nexport class HotkeyCommandItem extends Component {\n    static template = \"web.HotkeyCommandItem\";\n    static props = [\"hotkey\", \"hotkeyOptions?\", \"name?\", \"searchValue?\", \"executeCommand\", \"slots\"];\n    setup() {\n        useHotkey(this.props.hotkey, this.props.executeCommand);\n    }\n\n    getKeysToPress(command) {\n        const { hotkey } = command;\n        let result = hotkey.split(\"+\");\n        if (isMacOS()) {\n            result = result\n                .map((x) => x.replace(\"control\", \"command\"))\n                .map((x) => x.replace(\"alt\", \"control\"));\n        }\n        return result.map((key) => key.toUpperCase());\n    }\n}\n\nconst commandCategoryRegistry = registry.category(\"command_categories\");\nconst commandProviderRegistry = registry.category(\"command_provider\");\ncommandProviderRegistry.add(\"command\", {\n    provide: (env, options = {}) => {\n        const commands = env.services.command\n            .getCommands(options.activeElement)\n            .map((cmd) => {\n                cmd.category = commandCategoryRegistry.contains(cmd.category)\n                    ? cmd.category\n                    : \"default\";\n                return cmd;\n            })\n            .filter((command) => command.isAvailable === undefined || command.isAvailable());\n        // Filter out same category dupplicate commands\n        const uniqueCommands = commands.filter((obj, index) => {\n            return (\n                index ===\n                commands.findIndex((o) => obj.name === o.name && obj.category === o.category)\n            );\n        });\n        return uniqueCommands.map((command) => ({\n            Component: command.hotkey ? HotkeyCommandItem : DefaultCommandItem,\n            action: command.action,\n            category: command.category,\n            name: command.name,\n            props: {\n                hotkey: command.hotkey,\n                hotkeyOptions: command.hotkeyOptions,\n            },\n        }));\n    },\n});\n\ncommandProviderRegistry.add(\"data-hotkeys\", {\n    provide: (env, options = {}) => {\n        const commands = [];\n        const overlayModifier = registry.category(\"services\").get(\"hotkey\").overlayModifier;\n        // Also retrieve all hotkeyables elements\n        for (const el of getVisibleElements(\n            options.activeElement,\n            \"[data-hotkey]:not(:disabled)\"\n        )) {\n            const closest = el.closest(\"[data-command-category]\");\n            const category = closest ? closest.dataset.commandCategory : \"default\";\n            if (category === \"disabled\") {\n                continue;\n            }\n\n            const description =\n                el.title ||\n                el.dataset.bsOriginalTitle || // LEGACY: bootstrap moves title to data-bs-original-title\n                el.dataset.tooltip ||\n                el.placeholder ||\n                (el.innerText &&\n                    `${el.innerText.slice(0, 50)}${el.innerText.length > 50 ? \"...\" : \"\"}`) ||\n                _t(\"no description provided\");\n\n            commands.push({\n                Component: HotkeyCommandItem,\n                action: () => {\n                    // AAB: not sure it is enough, we might need to trigger all events that occur when you actually click\n                    el.focus();\n                    el.click();\n                },\n                category,\n                name: capitalize(description.trim().toLowerCase()),\n                props: {\n                    hotkey: `${overlayModifier}+${el.dataset.hotkey}`,\n                },\n            });\n        }\n        return commands;\n    },\n});\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { KeepLast, Race } from \"@web/core/utils/concurrency\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { scrollTo } from \"@web/core/utils/scrolling\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { isMacOS, isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\n\nimport {\n    Component,\n    onWillStart,\n    onWillDestroy,\n    EventBus,\n    useRef,\n    useState,\n    markRaw,\n    useExternalListener,\n} from \"@odoo/owl\";\n\nconst DEFAULT_PLACEHOLDER = _t(\"Search...\");\nconst DEFAULT_EMPTY_MESSAGE = _t(\"No result found\");\nconst FUZZY_NAMESPACES = [\"default\"];\n\n/**\n * @typedef {import(\"./command_service\").Command} Command\n */\n\n/**\n * @typedef {Command & {\n *  Component?: Component;\n *  props?: object;\n * }} CommandItem\n */\n\n/**\n * @typedef {{\n *  namespace?: string;\n *  provide: ()=>CommandItem[];\n * }} Provider\n */\n\n/**\n * @typedef {{\n *  categories: string[];\n *  debounceDelay: number;\n *  emptyMessage: string;\n *  placeholder: string;\n * }} NamespaceConfig\n */\n\n/**\n * @typedef {{\n *  configByNamespace?: {[namespace: string]: NamespaceConfig};\n *  FooterComponent?: Component;\n *  providers: Provider[];\n *  searchValue?: string;\n * }} CommandPaletteConfig\n */\n\n/**\n * Util used to filter commands that are within category.\n * Note: for the default category, also get all commands having invalid category.\n *\n * @param {string} categoryName the category key\n * @param {string[]} categories\n * @returns an array filter predicate\n */\nfunction commandsWithinCategory(categoryName, categories) {\n    return (cmd) => {\n        const inCurrentCategory = categoryName === cmd.category;\n        const fallbackCategory = categoryName === \"default\" && !categories.includes(cmd.category);\n        return inCurrentCategory || fallbackCategory;\n    };\n}\n\nexport function splitCommandName(name, searchValue) {\n    if (name) {\n        const splitName = name.split(new RegExp(`(${escapeRegExp(searchValue)})`, \"ig\"));\n        return searchValue.length && splitName.length > 1 ? splitName : [name];\n    }\n    return [];\n}\n\nexport class DefaultCommandItem extends Component {\n    static template = \"web.DefaultCommandItem\";\n    static props = {\n        slots: { type: Object, optional: true },\n        // Props send by the command palette:\n        hotkey: { type: String, optional: true },\n        hotkeyOptions: { type: String, optional: true },\n        name: { type: String, optional: true },\n        searchValue: { type: String, optional: true },\n        executeCommand: { type: Function, optional: true },\n    };\n}\n\nexport class CommandPalette extends Component {\n    static template = \"web.CommandPalette\";\n    static components = { Dialog };\n    static lastSessionId = 0;\n    static props = {\n        bus: { type: EventBus, optional: true },\n        close: Function,\n        config: Object,\n        closeMe: { type: Function, optional: true },\n    };\n\n    setup() {\n        if (this.props.bus) {\n            const setConfig = ({ detail }) => this.setCommandPaletteConfig(detail);\n            this.props.bus.addEventListener(`SET-CONFIG`, setConfig);\n            onWillDestroy(() => this.props.bus.removeEventListener(`SET-CONFIG`, setConfig));\n        }\n\n        this.keyId = 1;\n        this.race = new Race();\n        this.keepLast = new KeepLast();\n        this._sessionId = CommandPalette.lastSessionId++;\n        this.DefaultCommandItem = DefaultCommandItem;\n        this.activeElement = useService(\"ui\").activeElement;\n        this.inputRef = useAutofocus();\n\n        useHotkey(\"Enter\", () => this.executeSelectedCommand(), { bypassEditableProtection: true });\n        useHotkey(\"Control+Enter\", () => this.executeSelectedCommand(true), {\n            bypassEditableProtection: true,\n        });\n        useHotkey(\"ArrowUp\", () => this.selectCommandAndScrollTo(\"PREV\"), {\n            bypassEditableProtection: true,\n            allowRepeat: true,\n        });\n        useHotkey(\"ArrowDown\", () => this.selectCommandAndScrollTo(\"NEXT\"), {\n            bypassEditableProtection: true,\n            allowRepeat: true,\n        });\n        useExternalListener(window, \"mousedown\", this.onWindowMouseDown);\n\n        /**\n         * @type {{ commands: CommandItem[],\n         *          emptyMessage: string,\n         *          FooterComponent: Component,\n         *          namespace: string,\n         *          placeholder: string,\n         *          searchValue: string,\n         *          selectedCommand: CommandItem }}\n         */\n        this.state = useState({});\n\n        this.root = useRef(\"root\");\n        this.listboxRef = useRef(\"listbox\");\n\n        onWillStart(() => this.setCommandPaletteConfig(this.props.config));\n    }\n\n    get commandsByCategory() {\n        const categories = [];\n        for (const category of this.categoryKeys) {\n            const commands = this.state.commands.filter(\n                commandsWithinCategory(category, this.categoryKeys)\n            );\n            if (commands.length) {\n                categories.push({\n                    commands,\n                    name: this.categoryNames[category],\n                    keyId: category,\n                });\n            }\n        }\n        return categories;\n    }\n\n    /**\n     * Apply the new config to the command pallet\n     * @param {CommandPaletteConfig} config\n     */\n    async setCommandPaletteConfig(config) {\n        this.configByNamespace = config.configByNamespace || {};\n        this.state.FooterComponent = config.FooterComponent;\n\n        this.providersByNamespace = { default: [] };\n        for (const provider of config.providers) {\n            const namespace = provider.namespace || \"default\";\n            if (namespace in this.providersByNamespace) {\n                this.providersByNamespace[namespace].push(provider);\n            } else {\n                this.providersByNamespace[namespace] = [provider];\n            }\n        }\n\n        const { namespace, searchValue } = this.processSearchValue(config.searchValue || \"\");\n        this.switchNamespace(namespace);\n        this.state.searchValue = searchValue;\n        await this.race.add(this.search(searchValue));\n    }\n\n    /**\n     * Modifies the commands to be displayed according to the namespace and the options.\n     * Selects the first command in the new list.\n     * @param {string} namespace\n     * @param {object} options\n     */\n    async setCommands(namespace, options = {}) {\n        this.categoryKeys = [\"default\"];\n        this.categoryNames = {};\n        const proms = this.providersByNamespace[namespace].map((provider) => {\n            const { provide } = provider;\n            const result = provide(this.env, options);\n            return result;\n        });\n        let commands = (await this.keepLast.add(Promise.all(proms))).flat();\n        const namespaceConfig = this.configByNamespace[namespace] || {};\n        if (options.searchValue && FUZZY_NAMESPACES.includes(namespace)) {\n            commands = fuzzyLookup(options.searchValue, commands, (c) => c.name);\n        } else {\n            // we have to sort the commands by category to avoid navigation issues with the arrows\n            if (namespaceConfig.categories) {\n                let commandsSorted = [];\n                this.categoryKeys = namespaceConfig.categories;\n                this.categoryNames = namespaceConfig.categoryNames || {};\n                if (!this.categoryKeys.includes(\"default\")) {\n                    this.categoryKeys.push(\"default\");\n                }\n                for (const category of this.categoryKeys) {\n                    commandsSorted = commandsSorted.concat(\n                        commands.filter(commandsWithinCategory(category, this.categoryKeys))\n                    );\n                }\n                commands = commandsSorted;\n            }\n        }\n\n        this.state.commands = markRaw(\n            commands.slice(0, 100).map((command) => ({\n                ...command,\n                keyId: this.keyId++,\n                splitName: splitCommandName(command.name, options.searchValue),\n            }))\n        );\n        this.selectCommand(this.state.commands.length ? 0 : -1);\n        this.mouseSelectionActive = false;\n        this.state.emptyMessage = (\n            namespaceConfig.emptyMessage || DEFAULT_EMPTY_MESSAGE\n        ).toString();\n    }\n\n    selectCommand(index) {\n        if (index === -1 || index >= this.state.commands.length) {\n            this.state.selectedCommand = null;\n            return;\n        }\n        this.state.selectedCommand = markRaw(this.state.commands[index]);\n    }\n\n    selectCommandAndScrollTo(type) {\n        // In case the mouse is on the palette command, it avoids the selection\n        // of a command caused by a scroll.\n        this.mouseSelectionActive = false;\n        const index = this.state.commands.indexOf(this.state.selectedCommand);\n        if (index === -1) {\n            return;\n        }\n        let nextIndex;\n        if (type === \"NEXT\") {\n            nextIndex = index < this.state.commands.length - 1 ? index + 1 : 0;\n        } else if (type === \"PREV\") {\n            nextIndex = index > 0 ? index - 1 : this.state.commands.length - 1;\n        }\n        this.selectCommand(nextIndex);\n\n        const command = this.listboxRef.el.querySelector(`#o_command_${nextIndex}`);\n        scrollTo(command, { scrollable: this.listboxRef.el });\n    }\n\n    onCommandClicked(event, index) {\n        event.preventDefault(); // Prevent redirect for commands with href\n        this.selectCommand(index);\n        const ctrlKey = isMacOS() ? event.metaKey : event.ctrlKey;\n        this.executeSelectedCommand(ctrlKey);\n    }\n\n    /**\n     * Execute the action related to the order.\n     * If this action returns a config, then we will use it in the command palette,\n     * otherwise we close the command palette.\n     * @param {CommandItem} command\n     */\n    async executeCommand(command) {\n        const config = await command.action();\n        if (config) {\n            this.setCommandPaletteConfig(config);\n        } else {\n            this.props.close();\n        }\n    }\n\n    async executeSelectedCommand(ctrlKey) {\n        await this.searchValuePromise;\n        const selectedCommand = this.state.selectedCommand;\n        if (selectedCommand) {\n            if (!ctrlKey) {\n                this.executeCommand(selectedCommand);\n            } else if (selectedCommand.href) {\n                window.open(selectedCommand.href, \"_blank\");\n            }\n        }\n    }\n\n    onCommandMouseEnter(index) {\n        if (this.mouseSelectionActive) {\n            this.selectCommand(index);\n        } else {\n            this.mouseSelectionActive = true;\n        }\n    }\n\n    async search(searchValue) {\n        this.state.isLoading = true;\n        try {\n            await this.setCommands(this.state.namespace, {\n                searchValue,\n                activeElement: this.activeElement,\n                sessionId: this._sessionId,\n            });\n        } finally {\n            this.state.isLoading = false;\n        }\n        if (this.inputRef.el) {\n            this.inputRef.el.focus();\n        }\n    }\n\n    debounceSearch(value) {\n        const { namespace, searchValue } = this.processSearchValue(value);\n        if (namespace !== \"default\" && this.state.namespace !== namespace) {\n            this.switchNamespace(namespace);\n        }\n        this.state.searchValue = searchValue;\n        this.searchValuePromise = this.lastDebounceSearch(searchValue).catch(() => {\n            this.searchValuePromise = null;\n        });\n    }\n\n    onSearchInput(ev) {\n        this.debounceSearch(ev.target.value);\n    }\n\n    onKeyDown(ev) {\n        if (ev.key.toLowerCase() === \"backspace\" && !ev.target.value.length && !ev.repeat) {\n            this.switchNamespace(\"default\");\n            this.state.searchValue = \"\";\n            this.searchValuePromise = this.lastDebounceSearch(\"\").catch(() => {\n                this.searchValuePromise = null;\n            });\n        }\n    }\n\n    /**\n     * Close the palette on outside click.\n     */\n    onWindowMouseDown(ev) {\n        if (!this.root.el.contains(ev.target)) {\n            this.props.close();\n        }\n    }\n\n    switchNamespace(namespace) {\n        if (this.lastDebounceSearch) {\n            this.lastDebounceSearch.cancel();\n        }\n        const namespaceConfig = this.configByNamespace[namespace] || {};\n        this.lastDebounceSearch = debounce(\n            (value) => this.search(value),\n            namespaceConfig.debounceDelay || 0\n        );\n        this.state.namespace = namespace;\n        this.state.placeholder = namespaceConfig.placeholder || DEFAULT_PLACEHOLDER.toString();\n    }\n\n    processSearchValue(searchValue) {\n        let namespace = \"default\";\n        if (searchValue.length && this.providersByNamespace[searchValue[0]]) {\n            namespace = searchValue[0];\n            searchValue = searchValue.slice(1);\n        }\n        return { namespace, searchValue };\n    }\n\n    get isMacOS() {\n        return isMacOS();\n    }\n    get isMobileOS() {\n        return isMobileOS();\n    }\n}\n", "// This module makes it so that some errors only display a notification instead of an error dialog\n\nimport { registry } from \"@web/core/registry\";\nimport { odooExceptionTitleMap } from \"@web/core/errors/error_dialogs\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nodooExceptionTitleMap.forEach((title, exceptionName) => {\n    registry.category(\"error_notifications\").add(exceptionName, {\n        title: title,\n        type: \"warning\",\n        sticky: true,\n    });\n});\n\nconst sessionExpired = {\n    title: _t(\"Odoo Session Expired\"),\n    message: _t(\"Your Odoo session expired. The current page is about to be refreshed.\"),\n    buttons: [\n        {\n            text: _t(\"Ok\"),\n            click: () => window.location.reload(true),\n            close: true,\n        },\n    ],\n};\n\nregistry\n    .category(\"error_notifications\")\n    .add(\"odoo.http.SessionExpiredException\", sessionExpired)\n    .add(\"werkzeug.exceptions.Forbidden\", sessionExpired)\n    .add(\"504\", {\n        title: _t(\"Request timeout\"),\n        message: _t(\n            \"The operation was interrupted. This usually means that the current operation is taking too much time.\"\n        ),\n    });\n", "import { App } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nclass ComponentManager {\n    constructor(env) {\n        this.env = env;\n        this.appConfig = {\n            getTemplate,\n            env: env,\n            dev: env.debug,\n            translateFn: _t,\n            translatableAttributes: [\"data-tooltip\"],\n        };\n        /** @type {Map<HTMLElement, { app: App, mountProm: Promise<any> }>} */\n        this.apps = new Map();\n    }\n    async mountComponents() {\n        for (const [key, component] of registry.category(\"public_components\").getEntries()) {\n            for (const el of document.querySelectorAll(\n                `owl-component[name=\"${CSS.escape(key)}\"]`\n            )) {\n                if (!this.apps.has(el)) {\n                    const props = JSON.parse(el.getAttribute(\"props\") || \"{}\");\n\n                    const app = new App(component, {\n                        ...this.appConfig,\n                        props,\n                    });\n                    this.apps.set(el, { app, mountProm: app.mount(el) });\n                }\n            }\n        }\n        await Promise.all([...this.apps.values()].map(({ mountProm }) => mountProm));\n    }\n    destroyComponents() {\n        for (const { app } of this.apps.values()) {\n            app.destroy();\n        }\n        this.apps.clear();\n    }\n}\n\nexport const publicComponentService = {\n    start(env) {\n        return new ComponentManager(env);\n    },\n};\n\nregistry.category(\"services\").add(\"public_component\", publicComponentService);\n", "import {\n    deserializeDate,\n    deserializeDateTime,\n    parseDate,\n    parseDateTime,\n} from \"@web/core/l10n/dates\";\nimport PublicWidget from \"@web/legacy/js/public/public_widget\";\n\nexport const DateTimePickerWidget = PublicWidget.Widget.extend({\n    selector: \"[data-widget='datetime-picker']\",\n    disabledInEditableMode: true,\n\n    /**\n     * @override\n     */\n    start() {\n        this._super(...arguments);\n        const { widgetType, minDate, maxDate } = this.el.dataset;\n        const type = widgetType || \"datetime\";\n        const { value } = this.el;\n        const [parse, deserialize] =\n            type === \"date\" ? [parseDate, deserializeDate] : [parseDateTime, deserializeDateTime];\n        this.disableDateTimePicker = this.call(\"datetime_picker\", \"create\", {\n            target: this.el,\n            pickerProps: {\n                type,\n                minDate: minDate && deserialize(minDate),\n                maxDate: maxDate && deserialize(maxDate),\n                value: parse(value),\n            },\n        }).enable();\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this.disableDateTimePicker();\n        return this._super(...arguments);\n    },\n});\n\nPublicWidget.registry.DateTimePickerWidget = DateTimePickerWidget;\n", "/** @odoo-module **/\n\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { loadJS } from \"@web/core/assets\";\n\n/**\n * Until we have our own implementation of the /web/static/lib/pdfjs/web/viewer.{html,js,css}\n * (currently based on Firefox), this method allows us to hide the buttons that we do not want:\n * * \"Open File\"\n * * \"View Bookmark\"\n * * \"Print\" (Hidden on mobile device like Android, iOS, ...)\n * * \"Download\" (Hidden on mobile device like Android, iOS, ...)\n *\n * @link https://mozilla.github.io/pdf.js/getting_started/\n *\n * @param {Element} rootElement\n */\nexport function hidePDFJSButtons(rootElement) {\n    const cssStyle = document.createElement(\"style\");\n    cssStyle.rel = \"stylesheet\";\n    cssStyle.textContent = `button#secondaryOpenFile.secondaryToolbarButton, button#openFile.toolbarButton,\n    button#editorFreeText.toolbarButton, button#editorInk.toolbarButton, button#editorStamp.toolbarButton,\n    button#secondaryOpenFile.secondaryToolbarButton,\na#secondaryViewBookmark.secondaryToolbarButton, a#viewBookmark.toolbarButton {\ndisplay: none !important;\n}`;\n    if (isMobileOS()) {\n        cssStyle.textContent = `${cssStyle.innerHTML}\nbutton#secondaryDownload.secondaryToolbarButton, button#download.toolbarButton,\nbutton#editorFreeText.toolbarButton, button#editorInk.toolbarButton, button#editorStamp.toolbarButton,\nbutton#secondaryPrint.secondaryToolbarButton, button#print.toolbarButton{\ndisplay: none !important;\n}`;\n    }\n    const iframe =\n        rootElement.tagName === \"IFRAME\" ? rootElement : rootElement.querySelector(\"iframe\");\n    if (iframe) {\n        if (!iframe.dataset.hideButtons) {\n            iframe.dataset.hideButtons = \"true\";\n            iframe.addEventListener(\"load\", (event) => {\n                if (iframe.contentDocument && iframe.contentDocument.head) {\n                    iframe.contentDocument.head.appendChild(cssStyle);\n                }\n            });\n        }\n    } else {\n        console.warn(\"No IFRAME found\");\n    }\n}\n\nexport async function loadPDFJSAssets() {\n    return Promise.all([\n        loadJS(\"/web/static/lib/pdfjs/build/pdf.js\"),\n        loadJS(\"/web/static/lib/pdfjs/build/pdf.worker.js\"),\n    ]);\n}\n", "import { cookie } from \"@web/core/browser/cookie\";\nimport publicWidget from '@web/legacy/js/public/public_widget';\n\nimport lazyloader from \"@web/legacy/js/public/lazyloader\";\n\nimport { makeEnv, startServices } from \"@web/env\";\nimport { getTemplate } from '@web/core/templates';\nimport { MainComponentsContainer } from \"@web/core/main_components_container\";\nimport { browser } from '@web/core/browser/browser';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { jsToPyLocale, pyToJsLocale } from \"@web/core/l10n/utils\";\nimport { App, Component, whenReady } from \"@odoo/owl\";\nimport { RPCError } from '@web/core/network/rpc';\n\nconst { Settings } = luxon;\n\n// Load localizations outside the PublicRoot to not wait for DOM ready (but\n// wait for them in PublicRoot)\nfunction getLang() {\n    var html = document.documentElement;\n    return jsToPyLocale(html.getAttribute('lang')) || 'en_US';\n}\nconst lang = cookie.get('frontend_lang') || getLang(); // FIXME the cookie value should maybe be in the ctx?\n\n\n/**\n * Element which is designed to be unique and that will be the top-most element\n * in the widget hierarchy. So, all other widgets will be indirectly linked to\n * this Class instance. Its main role will be to retrieve RPC demands from its\n * children and handle them.\n */\nexport const PublicRoot = publicWidget.Widget.extend({\n    events: {\n        'submit .js_website_submit_form': '_onWebsiteFormSubmit',\n        'click .js_disable_on_click': '_onDisableOnClick',\n    },\n    custom_events: {\n        call_service: '_onCallService',\n        context_get: '_onContextGet',\n        main_object_request: '_onMainObjectRequest',\n        widgets_start_request: '_onWidgetsStartRequest',\n        widgets_stop_request: '_onWidgetsStopRequest',\n    },\n\n    /**\n     * @constructor\n     */\n    init: function (_, env) {\n        this._super.apply(this, arguments);\n        this.env = env;\n        this.publicWidgets = [];\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        var defs = [\n            this._super.apply(this, arguments),\n            this._startWidgets()\n        ];\n\n        // Display image thumbnail\n        this.$(\".o_image[data-mimetype^='image']\").each(function () {\n            var $img = $(this);\n            if (/gif|jpe|jpg|png|webp/.test($img.data('mimetype')) && $img.data('src')) {\n                $img.css('background-image', \"url('\" + $img.data('src') + \"')\");\n            }\n        });\n\n        // Auto scroll\n        if (window.location.hash.indexOf(\"scrollTop=\") > -1) {\n            this.el.scrollTop = +window.location.hash.match(/scrollTop=([0-9]+)/)[1];\n        }\n\n        return Promise.all(defs);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Retrieves the global context of the public environment. This is the\n     * context which is automatically added to each RPC.\n     *\n     * @private\n     * @param {Object} [context]\n     * @returns {Object}\n     */\n    _getContext: function (context) {\n        return Object.assign({\n            'lang': getLang(),\n        }, context || {});\n    },\n    /**\n     * Retrieves the global context of the public environment (as\n     * @see _getContext) but with extra informations that would be useless to\n     * send with each RPC.\n     *\n     * @private\n     * @param {Object} [context]\n     * @returns {Object}\n     */\n    _getExtraContext: function (context) {\n        return this._getContext(context);\n    },\n    /**\n     * @private\n     * @param {Object} [options]\n     * @returns {Object}\n     */\n    _getPublicWidgetsRegistry: function (options) {\n        return publicWidget.registry;\n    },\n    /**\n     * Creates an PublicWidget instance for each DOM element which matches the\n     * `selector` key of one of the registered widgets\n     * (@see PublicWidget.selector).\n     *\n     * @private\n     * @param {jQuery} [$from]\n     *        only initialize the public widgets whose `selector` matches the\n     *        element or one of its descendant (default to the wrapwrap element)\n     * @param {Object} [options]\n     * @returns {Deferred}\n     */\n    _startWidgets: function ($from, options) {\n        var self = this;\n\n        if ($from === undefined) {\n            $from = this.$('#wrapwrap');\n            if (!$from.length) {\n                // TODO Remove this once all frontend layouts possess a\n                // #wrapwrap element (which is necessary for those pages to be\n                // adapted correctly if the user installs website).\n                $from = this.$el;\n            }\n        }\n        options = Object.assign({}, options, {\n            wysiwyg: $('#wrapwrap').data('wysiwyg'),\n        });\n\n        this._stopWidgets($from);\n\n        var defs = Object.values(this._getPublicWidgetsRegistry(options)).map((PublicWidget) => {\n            const selector = PublicWidget.prototype.selector;\n            if (!selector) {\n                return;\n            }\n            const selectorHas = PublicWidget.prototype.selectorHas;\n            const selectorFunc = typeof selector === 'function'\n                ? selector\n                : fromEl => {\n                    const els = [...fromEl.querySelectorAll(selector)];\n                    if (fromEl.matches(selector)) {\n                        els.push(fromEl);\n                    }\n                    return els;\n                };\n\n            let targetEls = [];\n            for (const fromEl of $from) {\n                targetEls.push(...selectorFunc(fromEl));\n            }\n            if (selectorHas) {\n                targetEls = targetEls.filter(el => !!el.querySelector(selectorHas));\n            }\n\n            const proms = targetEls.map(el => {\n                var widget = new PublicWidget(self, options);\n                self.publicWidgets.push(widget);\n                return widget.attachTo(el);\n            });\n            return Promise.all(proms);\n        });\n        return Promise.all(defs);\n    },\n    /**\n     * Destroys all registered widget instances. Website would need this before\n     * saving while in edition mode for example.\n     *\n     * @private\n     * @param {jQuery} [$from]\n     *        only stop the public widgets linked to the given element(s) or one\n     *        of its descendants\n     */\n    _stopWidgets: function ($from) {\n        var removedWidgets = this.publicWidgets.map((widget) => {\n            if (!$from\n                || $from.filter(widget.el).length\n                || $from.find(widget.el).length) {\n                widget.destroy();\n                return widget;\n            }\n            return null;\n        });\n        this.publicWidgets = this.publicWidgets.filter((x) => removedWidgets.indexOf(x) < 0);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Calls the requested service from the env. Automatically adds the global\n     * context to RPCs.\n     *\n     * @private\n     * @param {OdooEvent} event\n     */\n    _onCallService: function (ev) {\n        const payload = ev.data;\n        const service = this.env.services[payload.service];\n        const result = service[payload.method].apply(service, payload.args || []);\n        payload.callback(result);\n        ev.stopPropagation();\n    },\n    /**\n     * Called when someone asked for the global public context.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onContextGet: function (ev) {\n        if (ev.data.extra) {\n            ev.data.callback(this._getExtraContext(ev.data.context));\n        } else {\n            ev.data.callback(this._getContext(ev.data.context));\n        }\n    },\n    /**\n     * Checks information about the page main object.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onMainObjectRequest: function (ev) {\n        var repr = $('html').data('main-object');\n        var m = repr.match(/(.+)\\((\\d+),(.*)\\)/);\n        ev.data.callback({\n            model: m[1],\n            id: m[2] | 0,\n        });\n    },\n    /**\n     * Called when the root is notified that the public widgets have to be\n     * (re)started.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onWidgetsStartRequest: function (ev) {\n        this._startWidgets(ev.data.$target, ev.data.options)\n            .then(ev.data.onSuccess)\n            .catch((e) => {\n                if (ev.data.onFailure) {\n                    ev.data.onFailure(e);\n                }\n                if (!(e instanceof RPCError)) {\n                    return Promise.reject(e);\n                }\n            });\n    },\n    /**\n     * Called when the root is notified that the public widgets have to be\n     * stopped.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onWidgetsStopRequest: function (ev) {\n        this._stopWidgets(ev.data.$target);\n    },\n    /**\n     * @todo review\n     * @private\n     */\n    _onWebsiteFormSubmit: function (ev) {\n        var $buttons = $(ev.currentTarget).find('button[type=\"submit\"], a.a-submit').toArray();\n        $buttons.forEach((btn) => {\n            var $btn = $(btn);\n            $btn.prepend('<i class=\"fa fa-circle-o-notch fa-spin\"></i> ');\n            $btn.prop('disabled', true);\n        });\n    },\n    /**\n     * Called when the root is notified that the button should be\n     * disabled after the first click.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onDisableOnClick: function (ev) {\n        $(ev.currentTarget).addClass('disabled');\n    },\n    /**\n     * Library clears the wrong date format so just ignore error\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onDateTimePickerError: function (ev) {\n        return false;\n    },\n});\n\n/**\n * This widget is important, because the tour manager needs a root widget in\n * order to work. The root widget must be a service provider with the ajax\n * service, so that the tour manager can let the server know when tours have\n * been consumed.\n */\nexport async function createPublicRoot(RootWidget) {\n    await lazyloader.allScriptsLoaded;\n    await whenReady();\n    const env = makeEnv();\n    await startServices(env);\n    Component.env = env;\n    await env.services.public_component.mountComponents();\n    const publicRoot = new RootWidget(null, env);\n    const app = new App(MainComponentsContainer, {\n        getTemplate,\n        env,\n        dev: env.debug,\n        translateFn: _t,\n        translatableAttributes: [\"data-tooltip\"],\n    });\n    const locale = pyToJsLocale(lang) || browser.navigator.language;\n    Settings.defaultLocale = locale;\n    const [root] = await Promise.all([\n        app.mount(document.body),\n        publicRoot.attachTo(document.body),\n    ]);\n    odoo.__WOWL_DEBUG__ = { root };\n    return publicRoot;\n}\n\nexport default { PublicRoot, createPublicRoot };\n", "/** @odoo-module alias=root.widget */\n\nimport { createPublicRoot } from \"@web/legacy/js/public/public_root\";\nimport lazyloader from \"@web/legacy/js/public/lazyloader\";\nimport { WebsiteRoot } from \"./website_root\";\nimport { loadBundle } from \"@web/core/assets\";\n\nconst prom = createPublicRoot(WebsiteRoot).then(async rootInstance => {\n    // This data attribute is set by the WebsitePreview client action for a\n    // restricted editor user.\n    if (window.frameElement && window.frameElement.dataset.loadWysiwyg === 'true') {\n        await loadBundle(\"website.assets_all_wysiwyg\");\n        window.dispatchEvent(new CustomEvent('PUBLIC-ROOT-READY', {detail: {rootInstance}}));\n    }\n    return rootInstance;\n});\nlazyloader.registerPageReadinessDelay(prom);\nexport default prom;\n", "/**\n * Provides a way to start JS code for public contents.\n */\n\nimport { Component } from \"@odoo/owl\";\nimport Class from \"@web/legacy/js/core/class\";\nimport { loadBundle, loadCSS, loadJS } from '@web/core/assets';\nimport { SERVICES_METADATA } from \"@web/core/utils/hooks\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { makeAsyncHandler, makeButtonHandler } from \"@web/legacy/js/public/minimal_dom\";\n\n/**\n * Mixin to structure objects' life-cycles following a parent-children\n * relationship. Each object can a have a parent and multiple children.\n * When an object is destroyed, all its children are destroyed too releasing\n * any resource they could have reserved before.\n *\n * @name ParentedMixin\n * @mixin\n */\nconst ParentedMixin = {\n    __parentedMixin: true,\n\n    init: function () {\n        this.__parentedDestroyed = false;\n        this.__parentedChildren = [];\n        this.__parentedParent = null;\n    },\n    /**\n     * Set the parent of the current object. When calling this method, the\n     * parent will also be informed and will return the current object\n     * when its getChildren() method is called. If the current object did\n     * already have a parent, it is unregistered before, which means the\n     * previous parent will not return the current object anymore when its\n     * getChildren() method is called.\n     */\n    setParent(parent) {\n        if (this.getParent()) {\n            if (this.getParent().__parentedMixin) {\n                const children = this.getParent().getChildren();\n                this.getParent().__parentedChildren = children.filter(\n                    (child) => child.$el !== this.$el\n                );\n            }\n        }\n        this.__parentedParent = parent;\n        if (parent && parent.__parentedMixin) {\n            parent.__parentedChildren.push(this);\n        }\n    },\n    /**\n     * Return the current parent of the object (or null).\n     */\n    getParent() {\n        return this.__parentedParent;\n    },\n    /**\n     * Return a list of the children of the current object.\n     */\n    getChildren() {\n        return [...this.__parentedChildren];\n    },\n    /**\n     * Returns true if destroy() was called on the current object.\n     */\n    isDestroyed() {\n        return this.__parentedDestroyed;\n    },\n    /**\n     * Releases any resource the instance could have reserved.\n     */\n    destroy() {\n        this.getChildren().forEach(function (child) {\n            child.destroy();\n        });\n        this.setParent(undefined);\n        this.__parentedDestroyed = true;\n    },\n};\n\nfunction OdooEvent(target, name, data) {\n    this.target = target;\n    this.name = name;\n    this.data = Object.create(null);\n    Object.assign(this.data, data);\n    this.stopped = false;\n}\nOdooEvent.prototype.stopPropagation = function () {\n    this.stopped = true;\n};\nOdooEvent.prototype.is_stopped = function () {\n    return this.stopped;\n};\n\n/**\n * Do not ever use it directly, use EventDispatcherMixin instead. This class\n * just handles the dispatching of events, it is not meant to be extended, nor\n * used directly. All integration with parenting and automatic unregistration of\n * events is done in EventDispatcherMixin.\n *\n * Copyright notice for the following Class and its uses:\n *\n * (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.\n * Backbone may be freely distributed under the MIT license.\n * For all details and documentation:\n * http://backbonejs.org\n *\n * See the debian/copyright file for the text of the MIT license.\n */\nclass Events {\n    on(events, callback, context) {\n        var ev;\n        events = events.split(/\\s+/);\n        var calls = this._callbacks || (this._callbacks = {});\n        while ((ev = events.shift())) {\n            var list = calls[ev] || (calls[ev] = {});\n            var tail = list.tail || (list.tail = list.next = {});\n            tail.callback = callback;\n            tail.context = context;\n            list.tail = tail.next = {};\n        }\n        return this;\n    }\n    off(events, callback, context) {\n        var ev, calls, node;\n        if (!events) {\n            delete this._callbacks;\n        } else if ((calls = this._callbacks)) {\n            events = events.split(/\\s+/);\n            while ((ev = events.shift())) {\n                node = calls[ev];\n                delete calls[ev];\n                if (!callback || !node) {\n                    continue;\n                }\n                while ((node = node.next) && node.next) {\n                    if (node.callback === callback\n                            && (!context || node.context === context)) {\n                        continue;\n                    }\n                    this.on(ev, node.callback, node.context);\n                }\n            }\n        }\n        return this;\n    }\n    callbackList() {\n        var lst = [];\n        for (const [eventName, el] of Object.entries(this._callbacks || {})) {\n            var node = el;\n            while ((node = node.next) && node.next) {\n                lst.push([eventName, node.callback, node.context]);\n            }\n        }\n        return lst;\n    }\n    trigger(events) {\n        var event, node, calls, tail, args, all, rest;\n        if (!(calls = this._callbacks)) {\n            return this;\n        }\n        all = calls.all;\n        (events = events.split(/\\s+/)).push(null);\n        // Save references to the current heads & tails.\n        while ((event = events.shift())) {\n            if (all) {\n                events.push({\n                    next: all.next,\n                    tail: all.tail,\n                    event: event\n                });\n            }\n            if (!(node = calls[event])) {\n                continue;\n            }\n            events.push({\n                next: node.next,\n                tail: node.tail\n            });\n        }\n        rest = Array.prototype.slice.call(arguments, 1);\n        while ((node = events.pop())) {\n            tail = node.tail;\n            args = node.event ? [node.event].concat(rest) : rest;\n            while ((node = node.next) !== tail) {\n                node.callback.apply(node.context || this, args);\n            }\n        }\n        return this;\n    }\n}\n\n/**\n * Mixin containing an event system. Events are also registered by specifying\n * the target object (the object which will receive the event when raised). Both\n * the event-emitting object and the target object store or reference to each\n * other. This is used to correctly remove all reference to the event handler\n * when any of the object is destroyed (when the destroy() method from\n * ParentedMixin is called). Removing those references is necessary to avoid\n * memory leak and phantom events (events which are raised and sent to a\n * previously destroyed object).\n *\n * @name EventDispatcherMixin\n * @mixin\n */\nconst EventDispatcherMixin = Object.assign({}, ParentedMixin, {\n    __eventDispatcherMixin: true,\n    \"custom_events\": {},\n\n    init() {\n        ParentedMixin.init.call(this);\n        this.__edispatcherEvents = new Events();\n        this.__edispatcherRegisteredEvents = [];\n        this._delegateCustomEvents();\n    },\n    /**\n     * Proxies a method of the object, in order to keep the right ``this`` on\n     * method invocations.\n     *\n     * This method is similar to ``Function.prototype.bind``, and\n     * even more so to ``jQuery.proxy`` with a fundamental difference: its\n     * resolution of the method being called is lazy, meaning it will use the\n     * method as it is when the proxy is called, not when the proxy is created.\n     *\n     * Other methods will fix the bound method to what it is when creating the\n     * binding/proxy, which is fine in most javascript code but problematic in\n     * Odoo where developers may want to replace existing callbacks with theirs.\n     *\n     * The semantics of this precisely replace closing over the method call.\n     *\n     * @param {String|Function} method function or name of the method to invoke\n     * @returns {Function} proxied method\n     */\n    proxy(method) {\n        var self = this;\n        return function () {\n            var fn = (typeof method === 'string') ? self[method] : method;\n            if (fn === void 0) {\n                throw new Error(\"Couldn't find method '\" + method + \"' in widget \" + self);\n            }\n            return fn.apply(self, arguments);\n        };\n    },\n    _delegateCustomEvents() {\n        if (Object.keys(this.custom_events || {}).length === 0) {\n            return;\n        }\n        for (var key in this.custom_events) {\n            if (!Object.prototype.hasOwnProperty.call(this.custom_events, key)) {\n                continue;\n            }\n\n            var method = this.proxy(this.custom_events[key]);\n            this.on(key, this, method);\n        }\n    },\n    on(events, dest, func) {\n        var self = this;\n        if (typeof func !== \"function\") {\n            throw new Error(\"Event handler must be a function.\");\n        }\n        events = events.split(/\\s+/);\n        events.forEach((eventName) => {\n            self.__edispatcherEvents.on(eventName, func, dest);\n            if (dest && dest.__eventDispatcherMixin) {\n                dest.__edispatcherRegisteredEvents.push({name: eventName, func: func, source: self});\n            }\n        });\n        return this;\n    },\n    off(events, dest, func) {\n        var self = this;\n        events = events.split(/\\s+/);\n        events.forEach((eventName) => {\n            self.__edispatcherEvents.off(eventName, func, dest);\n            if (dest && dest.__eventDispatcherMixin) {\n                dest.__edispatcherRegisteredEvents = dest.__edispatcherRegisteredEvents.filter(el => {\n                    return !(el.name === eventName && el.func === func && el.source === self);\n                });\n            }\n        });\n        return this;\n    },\n    trigger() {\n        this.__edispatcherEvents.trigger.apply(this.__edispatcherEvents, arguments);\n        return this;\n    },\n    \"trigger_up\": function (name, info) {\n        var event = new OdooEvent(this, name, info);\n        //console.info('event: ', name, info);\n        this._trigger_up(event);\n        return event;\n    },\n    \"_trigger_up\": function (event) {\n        var parent;\n        this.__edispatcherEvents.trigger(event.name, event);\n        if (!event.is_stopped() && (parent = this.getParent())) {\n            parent._trigger_up(event);\n        }\n    },\n    destroy() {\n        var self = this;\n        this.__edispatcherRegisteredEvents.forEach((event) => {\n            event.source.__edispatcherEvents.off(event.name, event.func, self);\n        });\n        this.__edispatcherRegisteredEvents = [];\n        this.__edispatcherEvents.callbackList().forEach(\n            ((cal) => {\n                this.off(cal[0], cal[2], cal[1]);\n            }).bind(this)\n        );\n        this.__edispatcherEvents.off();\n        ParentedMixin.destroy.call(this);\n    },\n});\n\nfunction protectMethod(widget, fn) {\n    return function (...args) {\n        return new Promise((resolve, reject) => {\n            Promise.resolve(fn.call(this, ...args))\n                .then((result) => {\n                    if (!widget.isDestroyed()) {\n                        resolve(result);\n                    }\n                })\n                .catch((reason) => {\n                    if (!widget.isDestroyed()) {\n                        reject(reason);\n                    }\n                });\n        });\n    };\n}\n\nconst ServicesMixin = {\n    bindService: function (serviceName) {\n        const { services } = Component.env;\n        const service = services[serviceName];\n        if (!service) {\n            throw new Error(`Service ${serviceName} is not available`);\n        }\n        if (serviceName in SERVICES_METADATA) {\n            if (service instanceof Function) {\n                return protectMethod(this, service);\n            } else {\n                const methods = SERVICES_METADATA[serviceName];\n                const result = Object.create(service);\n                for (const method of methods) {\n                    result[method] = protectMethod(this, service[method]);\n                }\n                return result;\n            }\n        }\n        return service;\n    },\n    /**\n     * @param  {string} service\n     * @param  {string} method\n     * @return {any} result of the service called\n     */\n    call: function (service, method) {\n        var args = Array.prototype.slice.call(arguments, 2);\n        var result;\n        this.trigger_up('call_service', {\n            service: service,\n            method: method,\n            args: args,\n            callback: function (r) {\n                result = r;\n            },\n        });\n        return result;\n    },\n};\n\n/**\n * Base class for all visual components. Provides a lot of functions helpful\n * for the management of a part of the DOM.\n *\n * Widget handles:\n *\n * - Rendering with QWeb.\n * - Life-cycle management and parenting (when a parent is destroyed, all its\n *   children are destroyed too).\n * - Insertion in DOM.\n *\n * **Guide to create implementations of the Widget class**\n *\n * Here is a sample child class::\n *\n *     var MyWidget = Widget.extend({\n *         // the name of the QWeb template to use for rendering\n *         template: \"MyQWebTemplate\",\n *\n *         init: function (parent) {\n *             this._super(parent);\n *             // stuff that you want to init before the rendering\n *         },\n *         willStart: function () {\n *             // async work that need to be done before the widget is ready\n *             // this method should return a promise\n *         },\n *         start: function() {\n *             // stuff you want to make after the rendering, `this.$el` holds a correct value\n *             this.$(\".my_button\").click(/* an example of event binding * /);\n *\n *             // if you have some asynchronous operations, it's a good idea to return\n *             // a promise in start(). Note that this is quite rare, and if you\n *             // need to fetch some data, this should probably be done in the\n *             // willStart method\n *             var promise = this._rpc(...);\n *             return promise;\n *         }\n *     });\n *\n * Now this class can simply be used with the following syntax::\n *\n *     var myWidget = new MyWidget(this);\n *     myWidget.appendTo($(\".some-div\"));\n *\n * With these two lines, the MyWidget instance was initialized, rendered,\n * inserted into the DOM inside the ``.some-div`` div and its events were\n * bound.\n *\n * This class can also be initialized and started on an existing DOM element\n * using the `selector` property. See below for more documentation.\n *\n * And of course, when you don't need that widget anymore, just do::\n *\n *     myWidget.destroy();\n *\n * That will kill the widget in a clean way and erase its content from the dom.\n *\n * This class also provides a way for executing code once a website DOM element\n * is loaded in the dom.\n * @see PublicWidget.selector\n */\nexport const PublicWidget = Class.extend(EventDispatcherMixin, ServicesMixin, {\n    // Backbone-ish API\n    tagName: 'div',\n    id: null,\n    className: null,\n    attributes: {},\n    /**\n     * The name of the QWeb template that will be used for rendering. Must be\n     * redefined in subclasses or the default render() method can not be used.\n     *\n     * @type {null|string}\n     */\n    template: null,\n    /**\n     * List of paths to css files that need to be loaded before the widget can\n     * be rendered. This will not induce loading anything that has already been\n     * loaded.\n     *\n     * @type {null|string[]}\n     */\n    cssLibs: null,\n    /**\n     * List of paths to js files that need to be loaded before the widget can\n     * be rendered. This will not induce loading anything that has already been\n     * loaded.\n     *\n     * @type {null|string[]}\n     */\n    jsLibs: null,\n    /**\n     * List of xmlID that need to be loaded before the widget can be rendered.\n     * The content css (link file or style tag) and js (file or inline) of the\n     * assets are loaded.\n     * This will not induce loading anything that has already been\n     * loaded.\n     *\n     * @type {null|string[]}\n     */\n    assetLibs: null,\n    /**\n     * The selector attribute, if defined, allows to automatically create an\n     * instance of this widget on page load for each DOM element according to\n     * this selector. The `PublicWidget.$el / el` element will then be that\n     * particular DOM element. This should be the main way of instantiating\n     * `PublicWidget` elements.\n     *\n     * The value can either be a string in which case it is considered as a\n     * `querySelectorAll` selector to match, or a function expecting to return\n     * all DOM elements to consider, which are inside the element received as\n     * parameter of the function (or that element itself).\n     *\n     * @see selectorHas\n     *\n     * @todo do not make this part of the Widget but rather an info to give when\n     * registering the widget.\n     *\n     * @type {string|function|false}\n     */\n    selector: false,\n    /**\n     * The `selectorHas` attribute, if defined, allows to filter elements found\n     * through the `selector` attribute by only considering those which contain\n     * at least an element which matches this `selectorHas` selector.\n     *\n     * Note that this is the equivalent of setting up a `selector` using the\n     * `:has` pseudo-selector but that pseudo-selector is known to not be fully\n     * supported in all browsers. To prevent useless crashes, using this\n     * `selectorHas` attribute should be preferred.\n     *\n     * @type {string|false}\n     */\n    selectorHas: false,\n    /**\n     * Extension of @see Widget.events\n     *\n     * A description of the event handlers to bind/delegate once the widget\n     * has been rendered::\n     *\n     *   'click .hello .world': 'async _onHelloWorldClick',\n     *     _^_      _^_           _^_        _^_\n     *      |        |             |          |\n     *      |  (Optional) jQuery   |  Handler method name\n     *      |  delegate selector   |\n     *      |                      |_ (Optional) space separated options\n     *      |                          * async: use the automatic system\n     *      |_ Event name with           making handlers promise-ready (see\n     *         potential jQuery          makeButtonHandler, makeAsyncHandler)\n     *         namespaces\n     *\n     * Note: the values may be replaced by a function declaration. This is\n     * however a deprecated behavior.\n     *\n     * @type {Object}\n     */\n    events: {},\n\n    /**\n     * @constructor\n     * @param {Object} parent\n     * @param {Object} [options]\n     */\n    init: function (parent, options) {\n        EventDispatcherMixin.init.call(this);\n        this.setParent(parent);\n        this.options = options || {};\n    },\n    /**\n     * Method called between @see init and @see start. Performs asynchronous\n     * calls required by the rendering and the start method.\n     *\n     * This method should return a Promise which is resolved when start can be\n     * executed.\n     *\n     * @returns {Promise}\n     */\n    willStart: function () {\n        var proms = [];\n        if (this.jsLibs || this.cssLibs || this.assetLibs) {\n            var assetsPromise = Promise.all([\n                ...(this.cssLibs || []).map(loadCSS),\n                ...(this.jsLibs || []).map(loadJS),\n            ]);\n            for (const bundleName of this.assetLibs || []) {\n                if (typeof bundleName === \"string\") {\n                    assetsPromise = assetsPromise.then(() => {\n                        return loadBundle(bundleName);\n                    });\n                } else {\n                    assetsPromise = assetsPromise.then(() => {\n                        return Promise.all([...bundleName.map(loadBundle)]);\n                    });\n                }\n            }\n            proms.push(assetsPromise);\n        }\n        return Promise.all(proms);\n    },\n    /**\n     * Method called after rendering. Mostly used to bind actions, perform\n     * asynchronous calls, etc...\n     *\n     * By convention, this method should return an object that can be passed to\n     * Promise.resolve() to inform the caller when this widget has been initialized.\n     *\n     * Note that, for historic reasons, many widgets still do work in the start\n     * method that would be more suited to the willStart method.\n     *\n     * @returns {Promise}\n     */\n    start: function () {\n        return Promise.resolve();\n    },\n    /**\n     * Destroys the widget and basically restores the target to the state it\n     * was before the start method was called (unlike standard widget, the\n     * associated $el DOM is not removed, if this was instantiated thanks to the\n     * selector property).\n     */\n    destroy: function () {\n        EventDispatcherMixin.destroy.call(this);\n        if (this.$el) {\n            this._undelegateEvents();\n\n            // If not done with a selector (attached to existing DOM), then\n            // remove the elements added to the DOM.\n            if (!this.selector) {\n                this.$el.remove();\n            }\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Renders the current widget and appends it to the given jQuery object.\n     *\n     * @param {jQuery} target\n     * @returns {Promise}\n     */\n    appendTo: function (target) {\n        var self = this;\n        return this._widgetRenderAndInsert(function (t) {\n            self.$el.appendTo(t);\n        }, target);\n    },\n    /**\n     * Attach the current widget to a dom element\n     *\n     * @param {jQuery} target\n     * @returns {Promise}\n     */\n    attachTo: function (target) {\n        var self = this;\n        this.setElement(target.$el || target);\n        return this.willStart().then(function () {\n            if (self.__parentedDestroyed) {\n                return;\n            }\n            return self.start();\n        });\n    },\n    /**\n     * Renders the current widget and inserts it after to the given jQuery\n     * object.\n     *\n     * @param {jQuery} target\n     * @returns {Promise}\n     */\n    insertAfter: function (target) {\n        var self = this;\n        return this._widgetRenderAndInsert(function (t) {\n            self.$el.insertAfter(t);\n        }, target);\n    },\n    /**\n     * Renders the current widget and inserts it before to the given jQuery\n     * object.\n     *\n     * @param {jQuery} target\n     * @returns {Promise}\n     */\n    insertBefore: function (target) {\n        var self = this;\n        return this._widgetRenderAndInsert(function (t) {\n            self.$el.insertBefore(t);\n        }, target);\n    },\n    /**\n     * Renders the current widget and prepends it to the given jQuery object.\n     *\n     * @param {jQuery} target\n     * @returns {Promise}\n     */\n    prependTo: function (target) {\n        var self = this;\n        return this._widgetRenderAndInsert(function (t) {\n            self.$el.prependTo(t);\n        }, target);\n    },\n    /**\n     * Renders the element. The default implementation renders the widget using\n     * QWeb, `this.template` must be defined. The context given to QWeb contains\n     * the \"widget\" key that references `this`.\n     */\n    renderElement: function () {\n        var $el;\n        if (this.template) {\n            $el = $(renderToElement(this.template, {widget: this}));\n        } else {\n            $el = this._makeDescriptive();\n        }\n        this._replaceElement($el);\n    },\n    /**\n     * Renders the current widget and replaces the given jQuery object.\n     *\n     * @param target A jQuery object or a Widget instance.\n     * @returns {Promise}\n     */\n    replace: function (target) {\n        return this._widgetRenderAndInsert((t) => {\n            this.$el.replaceAll(t);\n        }, target);\n    },\n    /**\n     * Re-sets the widget's root element (el/$el/$el).\n     *\n     * Includes:\n     *\n     * * re-delegating events\n     * * re-binding sub-elements\n     * * if the widget already had a root element, replacing the pre-existing\n     *   element in the DOM\n     *\n     * @param {HTMLElement | jQuery} element new root element for the widget\n     * @return {Widget} this\n     */\n    setElement: function (element) {\n        if (this.$el) {\n            this._undelegateEvents();\n        }\n\n        this.$el = (element instanceof $) ? element : $(element);\n        this.el = this.$el[0];\n\n        this._delegateEvents();\n\n        if (this.selector) {\n            this.$target = this.$el;\n            this.target = this.el;\n        }\n\n        return this;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Helper method, for ``this.$el.find(selector)``\n     *\n     * @private\n     * @param {string} selector CSS selector, rooted in $el\n     * @returns {jQuery} selector match\n     */\n    $: function (selector) {\n        if (selector === undefined) {\n            return this.$el;\n        }\n        return this.$el.find(selector);\n    },\n    /**\n     * @see this.events\n     * @override\n     */\n    _delegateEvents: function () {\n        var self = this;\n\n        const _delegateEvent = (method, key) => {\n            var match = /^(\\S+)(\\s+(.*))?$/.exec(key);\n            var event = match[1];\n            var selector = match[3];\n\n            event += '.widget_events';\n            if (!selector) {\n                self.$el.on(event, method);\n            } else {\n                self.$el.on(event, selector, method);\n            }\n        };\n        Object.entries(this.events || {}).forEach(([event, method]) => {\n            // If the method is a function, use the default Widget system\n            if (typeof method !== 'string') {\n                _delegateEvent(self.proxy(method), event);\n                return;\n            }\n            // If the method is only a function name without options, use the\n            // default Widget system\n            var methodOptions = method.split(' ');\n            if (methodOptions.length <= 1) {\n                _delegateEvent(self.proxy(method), event);\n                return;\n            }\n            // If the method has no meaningful options, use the default Widget\n            // system\n            var isAsync = methodOptions.includes('async');\n            if (!isAsync) {\n                _delegateEvent(self.proxy(method), event);\n                return;\n            }\n\n            method = self.proxy(methodOptions[methodOptions.length - 1]);\n            if (String(event).startsWith(\"click\")) {\n                // Protect click handler to be called multiple times by\n                // mistake by the user and add a visual disabling effect\n                // for buttons.\n                method = makeButtonHandler(method);\n            } else {\n                // Protect all handlers to be recalled while the previous\n                // async handler call is not finished.\n                method = makeAsyncHandler(method);\n            }\n            _delegateEvent(method, event);\n        });\n    },\n    /**\n     * @private\n     * @param {boolean} [extra=false]\n     * @param {Object} [extraContext]\n     * @returns {Object}\n     */\n    _getContext: function (extra, extraContext) {\n        var context;\n        this.trigger_up('context_get', {\n            extra: extra || false,\n            context: extraContext,\n            callback: function (ctx) {\n                context = ctx;\n            },\n        });\n        return context;\n    },\n    /**\n     * Makes a potential root element from the declarative builder of the\n     * widget\n     *\n     * @private\n     * @return {jQuery}\n     */\n    _makeDescriptive: function () {\n        var attrs = Object.assign({}, this.attributes || {});\n        if (this.id) {\n            attrs.id = this.id;\n        }\n        if (this.className) {\n            attrs['class'] = this.className;\n        }\n        var $el = $(document.createElement(this.tagName));\n        if (Object.keys(attrs || {}).length > 0) {\n            $el.attr(attrs);\n        }\n        return $el;\n    },\n    /**\n     * Re-sets the widget's root element and replaces the old root element\n     * (if any) by the new one in the DOM.\n     *\n     * @private\n     * @param {HTMLElement | jQuery} $el\n     * @returns {Widget} this instance, so it can be chained\n     */\n    _replaceElement: function ($el) {\n        var $oldel = this.$el;\n        this.setElement($el);\n        if ($oldel && !$oldel.is(this.$el)) {\n            if ($oldel.length > 1) {\n                $oldel.wrapAll('<div/>');\n                $oldel.parent().replaceWith(this.$el);\n            } else {\n                $oldel.replaceWith(this.$el);\n            }\n        }\n        return this;\n    },\n    /**\n     * Remove all handlers registered on this.$el\n     *\n     * @private\n     */\n    _undelegateEvents: function () {\n        this.$el.off('.widget_events');\n    },\n    /**\n     * Render the widget.  This is a private method, and should really never be\n     * called by anyone (except this widget).  It assumes that the widget was\n     * not willStarted yet.\n     *\n     * @private\n     * @param {function: jQuery -> any} insertion\n     * @param {jQuery} target\n     * @returns {Promise}\n     */\n    _widgetRenderAndInsert: function (insertion, target) {\n        var self = this;\n        return this.willStart().then(function () {\n            if (self.__parentedDestroyed) {\n                return;\n            }\n            self.renderElement();\n            insertion(target);\n            return self.start();\n        });\n    },\n});\n\n//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\n\n/**\n * The registry object contains the list of widgets that should be instantiated\n * thanks to their selector property if any.\n */\nvar registry = {};\n\nexport default {\n    Widget: PublicWidget,\n    registry: registry,\n\n    ParentedMixin: ParentedMixin,\n    EventDispatcherMixin: EventDispatcherMixin,\n    ServicesMixin: ServicesMixin,\n};\n", "import publicWidget from '@web/legacy/js/public/public_widget';\nimport { addLoadingEffect } from '@web/core/utils/ui';\n\npublicWidget.registry.login = publicWidget.Widget.extend({\n    selector: '.oe_login_form',\n    events: {\n        'submit': '_onSubmit',\n    },\n\n    //-------------------------------------------------------------------------\n    // Handlers\n    //-------------------------------------------------------------------------\n\n    /**\n     * Prevents the user from crazy clicking:\n     * Gives the button a loading effect if preventDefault was not already\n     * called and modifies the preventDefault function of the event so that the\n     * loading effect is removed if preventDefault() is called in a following\n     * customization.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onSubmit(ev) {\n        if (!ev.defaultPrevented) {\n            const btnEl = ev.currentTarget.querySelector('button[type=\"submit\"]');\n            const removeLoadingEffect = addLoadingEffect(btnEl);\n            const oldPreventDefault = ev.preventDefault.bind(ev);\n            ev.preventDefault = () => {\n                removeLoadingEffect();\n                oldPreventDefault();\n            };\n        }\n    },\n});\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\n\nexport const busParametersService = {\n    start() {\n        return {\n            serverURL: window.origin,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"bus.parameters\", busParametersService);\n", "/** @odoo-module **/\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { session } from \"@web/session\";\n\nexport const AWAY_DELAY = 30 * 60 * 1000; // 30 minutes\nexport const FIRST_UPDATE_DELAY = 500;\nexport const UPDATE_BUS_PRESENCE_DELAY = 60000;\n\n/**\n * This service keeps the user's presence up to date with the server. When the\n * connection to the server is established, the user's presence is updated. If\n * another device or browser updates the user's presence, the presence is sent to\n * the server if relevant (e.g., another device is away or offline, but this one\n * is online). To receive updates through the bus, subscribe to presence channels\n * (e.g., subscribe to `odoo-presence-res.partner_3` to receive updates about\n * this partner).\n */\nexport const imStatusService = {\n    dependencies: [\"bus_service\", \"presence\"],\n\n    start(env, { bus_service, presence }) {\n        let lastSentInactivity;\n        let becomeAwayTimeout;\n\n        const updateBusPresence = () => {\n            lastSentInactivity = presence.getInactivityPeriod();\n            startAwayTimeout();\n            bus_service.send(\"update_presence\", {\n                inactivity_period: lastSentInactivity,\n                im_status_ids_by_model: {},\n            });\n        };\n        this.updateBusPresence = updateBusPresence;\n\n        const startAwayTimeout = () => {\n            clearTimeout(becomeAwayTimeout);\n            const awayTime = AWAY_DELAY - presence.getInactivityPeriod();\n            if (awayTime > 0) {\n                becomeAwayTimeout = browser.setTimeout(() => updateBusPresence(), awayTime);\n            }\n        };\n        bus_service.addEventListener(\"connect\", () => updateBusPresence(), { once: true });\n        bus_service.subscribe(\"bus.bus/im_status_updated\", async ({ partner_id, im_status }) => {\n            if (session.is_public || !partner_id || partner_id !== user.partnerId) {\n                return;\n            }\n            const isOnline = presence.getInactivityPeriod() < AWAY_DELAY;\n            if (im_status === \"offline\" || (im_status === \"away\" && isOnline)) {\n                this.updateBusPresence();\n            }\n        });\n        presence.bus.addEventListener(\"presence\", () => {\n            if (lastSentInactivity >= AWAY_DELAY) {\n                this.updateBusPresence();\n            }\n            startAwayTimeout();\n        });\n    },\n};\n\nregistry.category(\"services\").add(\"im_status\", imStatusService);\n", "/** @odoo-module */\n\nimport { browser } from \"@web/core/browser/browser\";\n\n/**\n * Returns a function, that, when invoked, will only be triggered at most once\n * during a given window of time. Normally, the throttled function will run\n * as much as it can, without ever going more than once per `wait` duration;\n * but if you'd like to disable the execution on the leading edge, pass\n * `{leading: false}`. To disable execution on the trailing edge, ditto.\n *\n * credit to `underscore.js`\n */\nfunction throttle(func, wait, options) {\n    let timeout, context, args, result;\n    let previous = 0;\n    if (!options) {\n        options = {};\n    }\n\n    const later = function () {\n        previous = options.leading === false ? 0 : luxon.DateTime.now().ts;\n        timeout = null;\n        result = func.apply(context, args);\n        if (!timeout) {\n            context = args = null;\n        }\n    };\n\n    const throttled = function () {\n        const _now = luxon.DateTime.now().ts;\n        if (!previous && options.leading === false) {\n            previous = _now;\n        }\n        const remaining = wait - (_now - previous);\n        context = this;\n        args = arguments;\n        if (remaining <= 0 || remaining > wait) {\n            if (timeout) {\n                browser.clearTimeout(timeout);\n                timeout = null;\n            }\n            previous = _now;\n            result = func.apply(context, args);\n            if (!timeout) {\n                context = args = null;\n            }\n        } else if (!timeout && options.trailing !== false) {\n            timeout = browser.setTimeout(later, remaining);\n        }\n        return result;\n    };\n\n    throttled.cancel = function () {\n        browser.clearTimeout(timeout);\n        previous = 0;\n        timeout = context = args = null;\n    };\n\n    return throttled;\n}\n\nexport const timings = {\n    throttle,\n};\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { EventBus } from \"@odoo/owl\";\n\nlet multiTabId = 0;\n/**\n * This service uses a Master/Slaves with Leader Election architecture in\n * order to keep track of the main tab. Tabs are synchronized thanks to the\n * localStorage.\n *\n * localStorage used keys are:\n * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.lastPresenceByTab:\n *   mapping of tab ids to their last recorded presence.\n * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.main : id of the current\n *   main tab.\n * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.heartbeat : last main tab\n *   heartbeat time.\n *\n * trigger:\n * - become_main_tab : when this tab became the main.\n * - no_longer_main_tab : when this tab is no longer the main.\n * - shared_value_updated: when one of the shared values changes.\n */\nexport const multiTabService = {\n    start() {\n        const bus = new EventBus();\n\n        // CONSTANTS\n        const TAB_HEARTBEAT_PERIOD = 10000; // 10 seconds\n        const MAIN_TAB_HEARTBEAT_PERIOD = 1500; // 1.5 seconds\n        const HEARTBEAT_OUT_OF_DATE_PERIOD = 5000; // 5 seconds\n        const HEARTBEAT_KILL_OLD_PERIOD = 15000; // 15 seconds\n        // Keys that should not trigger the `shared_value_updated` event.\n        const PRIVATE_LOCAL_STORAGE_KEYS = [\"main\", \"heartbeat\"];\n\n        // PROPERTIES\n        let _isOnMainTab = false;\n        let lastHeartbeat = 0;\n        let heartbeatTimeout;\n        const sanitizedOrigin = location.origin.replace(/:\\/{0,2}/g, \"_\");\n        const localStoragePrefix = `${this.name}.${sanitizedOrigin}.`;\n        const now = new Date().getTime();\n        const tabId = `${this.name}${multiTabId++}:${now}`;\n\n        function generateLocalStorageKey(baseKey) {\n            return localStoragePrefix + baseKey;\n        }\n\n        function getItemFromStorage(key, defaultValue) {\n            const item = browser.localStorage.getItem(generateLocalStorageKey(key));\n            try {\n                return item ? JSON.parse(item) : defaultValue;\n            } catch {\n                return item;\n            }\n        }\n\n        function setItemInStorage(key, value) {\n            browser.localStorage.setItem(generateLocalStorageKey(key), JSON.stringify(value));\n        }\n\n        function startElection() {\n            if (_isOnMainTab) {\n                return;\n            }\n            // Check who's next.\n            const now = new Date().getTime();\n            const lastPresenceByTab = getItemFromStorage(\"lastPresenceByTab\", {});\n            const heartbeatKillOld = now - HEARTBEAT_KILL_OLD_PERIOD;\n            let newMain;\n            for (const [tab, lastPresence] of Object.entries(lastPresenceByTab)) {\n                // Check for dead tabs.\n                if (lastPresence < heartbeatKillOld) {\n                    continue;\n                }\n                newMain = tab;\n                break;\n            }\n            if (newMain === tabId) {\n                // We're next in queue. Electing as main.\n                lastHeartbeat = now;\n                setItemInStorage(\"heartbeat\", lastHeartbeat);\n                setItemInStorage(\"main\", true);\n                _isOnMainTab = true;\n                bus.trigger(\"become_main_tab\");\n                // Removing main peer from queue.\n                delete lastPresenceByTab[newMain];\n                setItemInStorage(\"lastPresenceByTab\", lastPresenceByTab);\n            }\n        }\n\n        function heartbeat() {\n            const now = new Date().getTime();\n            let heartbeatValue = getItemFromStorage(\"heartbeat\", 0);\n            const lastPresenceByTab = getItemFromStorage(\"lastPresenceByTab\", {});\n            if (heartbeatValue + HEARTBEAT_OUT_OF_DATE_PERIOD < now) {\n                // Heartbeat is out of date. Electing new main.\n                startElection();\n                heartbeatValue = getItemFromStorage(\"heartbeat\", 0);\n            }\n            if (_isOnMainTab) {\n                // Walk through all tabs and kill old ones.\n                const cleanedTabs = {};\n                for (const [tabId, lastPresence] of Object.entries(lastPresenceByTab)) {\n                    if (lastPresence + HEARTBEAT_KILL_OLD_PERIOD > now) {\n                        cleanedTabs[tabId] = lastPresence;\n                    }\n                }\n                if (heartbeatValue !== lastHeartbeat) {\n                    // Someone else is also main...\n                    // It should not happen, except in some race condition situation.\n                    _isOnMainTab = false;\n                    lastHeartbeat = 0;\n                    lastPresenceByTab[tabId] = now;\n                    setItemInStorage(\"lastPresenceByTab\", lastPresenceByTab);\n                    bus.trigger(\"no_longer_main_tab\");\n                } else {\n                    lastHeartbeat = now;\n                    setItemInStorage(\"heartbeat\", now);\n                    setItemInStorage(\"lastPresenceByTab\", cleanedTabs);\n                }\n            } else {\n                // Update own heartbeat.\n                lastPresenceByTab[tabId] = now;\n                setItemInStorage(\"lastPresenceByTab\", lastPresenceByTab);\n            }\n            const hbPeriod = _isOnMainTab ? MAIN_TAB_HEARTBEAT_PERIOD : TAB_HEARTBEAT_PERIOD;\n            heartbeatTimeout = browser.setTimeout(heartbeat, hbPeriod);\n        }\n\n        function onStorage({ key, newValue }) {\n            if (key === generateLocalStorageKey(\"main\") && !newValue) {\n                // Main was unloaded.\n                startElection();\n            }\n            if (PRIVATE_LOCAL_STORAGE_KEYS.includes(key)) {\n                return;\n            }\n            if (key && key.includes(localStoragePrefix)) {\n                // Only trigger the shared_value_updated event if the key is\n                // related to this service/origin.\n                const baseKey = key.replace(localStoragePrefix, \"\");\n                bus.trigger(\"shared_value_updated\", { key: baseKey, newValue });\n            }\n        }\n\n        /**\n         * Unregister this tab from the multi-tab service. It will no longer\n         * be able to become the main tab.\n         */\n        function unregister() {\n            clearTimeout(heartbeatTimeout);\n            const lastPresenceByTab = getItemFromStorage(\"lastPresenceByTab\", {});\n            delete lastPresenceByTab[tabId];\n            setItemInStorage(\"lastPresenceByTab\", lastPresenceByTab);\n\n            // Unload main.\n            if (_isOnMainTab) {\n                _isOnMainTab = false;\n                bus.trigger(\"no_longer_main_tab\");\n                browser.localStorage.removeItem(generateLocalStorageKey(\"main\"));\n            }\n        }\n\n        browser.addEventListener(\"pagehide\", unregister);\n        browser.addEventListener(\"storage\", onStorage);\n\n        // REGISTER THIS TAB\n        const lastPresenceByTab = getItemFromStorage(\"lastPresenceByTab\", {});\n        lastPresenceByTab[tabId] = now;\n        setItemInStorage(\"lastPresenceByTab\", lastPresenceByTab);\n\n        if (!getItemFromStorage(\"main\")) {\n            startElection();\n        }\n        heartbeat();\n\n        return {\n            bus,\n            get currentTabId() {\n                return tabId;\n            },\n            /**\n             * Determine whether or not this tab is the main one.\n             *\n             * @returns {boolean}\n             */\n            isOnMainTab() {\n                return _isOnMainTab;\n            },\n            /**\n             * Get value shared between all the tabs.\n             *\n             * @param {string} key\n             * @param {any} defaultValue Value to be returned if this\n             * key does not exist.\n             */\n            getSharedValue(key, defaultValue) {\n                return getItemFromStorage(key, defaultValue);\n            },\n            /**\n             * Set value shared between all the tabs.\n             *\n             * @param {string} key\n             * @param {any} value\n             */\n            setSharedValue(key, value) {\n                if (value === undefined) {\n                    return this.removeSharedValue(key);\n                }\n                setItemInStorage(key, value);\n            },\n            /**\n             * Remove value shared between all the tabs.\n             *\n             * @param {string} key\n             */\n            removeSharedValue(key) {\n                browser.localStorage.removeItem(generateLocalStorageKey(key));\n            },\n            /**\n             * Unregister this tab from the multi-tab service. It will no longer\n             * be able to become the main tab.\n             */\n            unregister: unregister,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"multi_tab\", multiTabService);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { isIosApp } from \"@web/core/browser/feature_detection\";\nimport { EventBus } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\n\n// List of worker events that should not be broadcasted.\nconst INTERNAL_EVENTS = new Set([\"initialized\", \"outdated\", \"notification\", \"update_state\"]);\n/**\n * Communicate with a SharedWorker in order to provide a single websocket\n * connection shared across multiple tabs.\n *\n *  @emits connect\n *  @emits disconnect\n *  @emits reconnect\n *  @emits reconnecting\n */\nexport const busService = {\n    dependencies: [\"bus.parameters\", \"localization\", \"multi_tab\", \"notification\"],\n\n    start(env, { multi_tab: multiTab, notification, \"bus.parameters\": params }) {\n        const bus = new EventBus();\n        const notificationBus = new EventBus();\n        const subscribeFnToWrapper = new Map();\n        let worker;\n        /**\n         * @typedef {typeof import(\"@bus/workers/websocket_worker\").WORKER_STATE} WORKER_STATE\n         * @type {WORKER_STATE[keyof WORKER_STATE]}\n         */\n        let workerState;\n        let isActive = false;\n        let isInitialized = false;\n        let isUsingSharedWorker = browser.SharedWorker && !isIosApp();\n        const startedAt = luxon.DateTime.now().set({ milliseconds: 0 });\n        const connectionInitializedDeferred = new Deferred();\n\n        /**\n         * Send a message to the worker.\n         *\n         * @param {WorkerAction} action Action to be\n         * executed by the worker.\n         * @param {Object|undefined} data Data required for the action to be\n         * executed.\n         */\n        function send(action, data) {\n            if (!worker) {\n                return;\n            }\n            const message = { action, data };\n            if (isUsingSharedWorker) {\n                worker.port.postMessage(message);\n            } else {\n                worker.postMessage(message);\n            }\n        }\n\n        /**\n         * Handle messages received from the shared worker and fires an\n         * event according to the message type.\n         *\n         * @param {MessageEvent} messageEv\n         * @param {{type: WorkerEvent, data: any}[]}  messageEv.data\n         */\n        function handleMessage(messageEv) {\n            const { type, data } = messageEv.data;\n            switch (type) {\n                case \"notification\": {\n                    const notifications = data.map(({ id, message }) => ({ id, ...message }));\n                    multiTab.setSharedValue(\"last_notification_id\", notifications.at(-1).id);\n                    for (const { id, type, payload } of notifications) {\n                        notificationBus.trigger(type, { id, payload });\n                        busService._onMessage(id, type, payload);\n                    }\n                    break;\n                }\n                case \"initialized\": {\n                    isInitialized = true;\n                    connectionInitializedDeferred.resolve();\n                    break;\n                }\n                case \"update_state\":\n                    workerState = data;\n                    break;\n                case \"outdated\": {\n                    multiTab.unregister();\n                    notification.add(\n                        _t(\n                            \"Save your work and refresh to get the latest updates and avoid potential issues.\"\n                        ),\n                        {\n                            title: _t(\"The page is out of date\"),\n                            type: \"warning\",\n                            sticky: true,\n                            buttons: [\n                                {\n                                    name: _t(\"Refresh\"),\n                                    primary: true,\n                                    onClick: () => {\n                                        browser.location.reload();\n                                    },\n                                },\n                            ],\n                        }\n                    );\n                    break;\n                }\n            }\n            if (!INTERNAL_EVENTS.has(type)) {\n                bus.trigger(type, data);\n            }\n        }\n\n        /**\n         * Initialize the connection to the worker by sending it usefull\n         * initial informations (last notification id, debug mode,\n         * ...).\n         */\n        function initializeWorkerConnection() {\n            // User_id has different values according to its origin:\n            //     - user service : number or false (key: userId)\n            //     - guest page: array containing null or number\n            //     - public pages: undefined\n            // Let's format it in order to ease its usage:\n            //     - number if user is logged, false otherwise, keep\n            //       undefined to indicate session_info is not available.\n            let uid = Array.isArray(session.user_id) ? session.user_id[0] : user.userId;\n            if (!uid && uid !== undefined) {\n                uid = false;\n            }\n            send(\"initialize_connection\", {\n                websocketURL: `${params.serverURL.replace(\"http\", \"ws\")}/websocket?version=${\n                    session.websocket_worker_version\n                }`,\n                db: session.db,\n                debug: odoo.debug,\n                lastNotificationId: multiTab.getSharedValue(\"last_notification_id\", 0),\n                uid,\n                startTs: startedAt.valueOf(),\n            });\n        }\n\n        /**\n         * Start the \"bus_service\" worker.\n         */\n        function startWorker() {\n            let workerURL = `${params.serverURL}/bus/websocket_worker_bundle?v=${session.websocket_worker_version}`;\n            if (params.serverURL !== window.origin) {\n                // Bus service is loaded from a different origin than the bundle\n                // URL. The Worker expects an URL from this origin, give it a base64\n                // URL that will then load the bundle via \"importScripts\" which\n                // allows cross origin.\n                const source = `importScripts(\"${workerURL}\");`;\n                workerURL = \"data:application/javascript;base64,\" + window.btoa(source);\n            }\n            const workerClass = isUsingSharedWorker ? browser.SharedWorker : browser.Worker;\n            worker = new workerClass(workerURL, {\n                name: isUsingSharedWorker\n                    ? \"odoo:websocket_shared_worker\"\n                    : \"odoo:websocket_worker\",\n            });\n            worker.addEventListener(\"error\", (e) => {\n                if (!isInitialized && workerClass === browser.SharedWorker) {\n                    console.warn(\n                        'Error while loading \"bus_service\" SharedWorker, fallback on Worker.'\n                    );\n                    isUsingSharedWorker = false;\n                    startWorker();\n                } else if (!isInitialized) {\n                    isInitialized = true;\n                    connectionInitializedDeferred.resolve();\n                    console.warn(\"Bus service failed to initialized.\");\n                }\n            });\n            if (isUsingSharedWorker) {\n                worker.port.start();\n                worker.port.addEventListener(\"message\", handleMessage);\n            } else {\n                worker.addEventListener(\"message\", handleMessage);\n            }\n            initializeWorkerConnection();\n        }\n        browser.addEventListener(\"pagehide\", ({ persisted }) => {\n            if (!persisted) {\n                // Page is gonna be unloaded, disconnect this client\n                // from the worker.\n                send(\"leave\");\n            }\n        });\n        browser.addEventListener(\"online\", () => {\n            if (isActive) {\n                send(\"start\");\n            }\n        });\n        browser.addEventListener(\"offline\", () => send(\"stop\"));\n\n        return {\n            addEventListener: bus.addEventListener.bind(bus),\n            addChannel: async (channel) => {\n                if (!worker) {\n                    startWorker();\n                    await connectionInitializedDeferred;\n                }\n                send(\"add_channel\", channel);\n                send(\"start\");\n                isActive = true;\n            },\n            deleteChannel: (channel) => send(\"delete_channel\", channel),\n            forceUpdateChannels: () => send(\"force_update_channels\"),\n            trigger: bus.trigger.bind(bus),\n            removeEventListener: bus.removeEventListener.bind(bus),\n            send: (eventName, data) => send(\"send\", { event_name: eventName, data }),\n            start: async () => {\n                if (!worker) {\n                    startWorker();\n                    await connectionInitializedDeferred;\n                }\n                send(\"start\");\n                isActive = true;\n            },\n            stop: () => {\n                send(\"leave\");\n                isActive = false;\n            },\n            get isActive() {\n                return isActive;\n            },\n            /**\n             * Subscribe to a single notification type.\n             *\n             * @param {string} notificationType\n             * @param {function} callback\n             */\n            subscribe(notificationType, callback) {\n                const wrapper = ({ detail }) => {\n                    const { id, payload } = detail;\n                    callback(payload, { id });\n                };\n                subscribeFnToWrapper.set(callback, wrapper);\n                notificationBus.addEventListener(notificationType, wrapper);\n            },\n            /**\n             * Unsubscribe from a single notification type.\n             *\n             * @param {string} notificationType\n             * @param {function} callback\n             */\n            unsubscribe(notificationType, callback) {\n                notificationBus.removeEventListener(\n                    notificationType,\n                    subscribeFnToWrapper.get(callback)\n                );\n                subscribeFnToWrapper.delete(callback);\n            },\n            startedAt,\n            get workerState() {\n                return workerState;\n            },\n        };\n    },\n    /** Overriden to provide logs in tests. Use subscribe() in production. */\n    _onMessage(id, type, payload) {},\n};\nregistry.category(\"services\").add(\"bus_service\", busService);\n", "/** @odoo-module **/\n\nimport { EventBus } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\n\nexport const presenceService = {\n    start(env) {\n        const LOCAL_STORAGE_PREFIX = \"presence\";\n        const bus = new EventBus();\n        let isOdooFocused = true;\n        let lastPresenceTime =\n            browser.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}.lastPresence`) ||\n            luxon.DateTime.now().ts;\n\n        function onPresence() {\n            lastPresenceTime = luxon.DateTime.now().ts;\n            browser.localStorage.setItem(`${LOCAL_STORAGE_PREFIX}.lastPresence`, lastPresenceTime);\n            bus.trigger(\"presence\");\n        }\n\n        function onFocusChange(isFocused) {\n            try {\n                isFocused = parent.document.hasFocus();\n            } catch {\n                // noop\n            }\n            isOdooFocused = isFocused;\n            browser.localStorage.setItem(`${LOCAL_STORAGE_PREFIX}.focus`, isOdooFocused);\n            if (isOdooFocused) {\n                lastPresenceTime = luxon.DateTime.now().ts;\n                env.bus.trigger(\"window_focus\", isOdooFocused);\n            }\n        }\n\n        function onStorage({ key, newValue }) {\n            if (key === `${LOCAL_STORAGE_PREFIX}.focus`) {\n                isOdooFocused = JSON.parse(newValue);\n                env.bus.trigger(\"window_focus\", newValue);\n            }\n            if (key === `${LOCAL_STORAGE_PREFIX}.lastPresence`) {\n                lastPresenceTime = JSON.parse(newValue);\n                bus.trigger(\"presence\");\n            }\n        }\n        browser.addEventListener(\"storage\", onStorage);\n        browser.addEventListener(\"focus\", () => onFocusChange(true));\n        browser.addEventListener(\"blur\", () => onFocusChange(false));\n        browser.addEventListener(\"pagehide\", () => onFocusChange(false));\n        browser.addEventListener(\"click\", onPresence);\n        browser.addEventListener(\"keydown\", onPresence);\n\n        return {\n            bus,\n            getLastPresence() {\n                return lastPresenceTime;\n            },\n            isOdooFocused() {\n                return isOdooFocused;\n            },\n            getInactivityPeriod() {\n                return luxon.DateTime.now().ts - this.getLastPresence();\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"presence\", presenceService);\n", "/** @odoo-module **/\n\nimport { debounce, Deferred } from \"@bus/workers/websocket_worker_utils\";\n\n/**\n * Type of events that can be sent from the worker to its clients.\n *\n * @typedef { 'connect' | 'reconnect' | 'disconnect' | 'reconnecting' | 'notification' | 'initialized' | 'outdated'|'update_state'} WorkerEvent\n */\n\n/**\n * Type of action that can be sent from the client to the worker.\n *\n * @typedef {'add_channel' | 'delete_channel' | 'force_update_channels' | 'initialize_connection' | 'send' | 'leave' | 'stop' | 'start'} WorkerAction\n */\n\nexport const WEBSOCKET_CLOSE_CODES = Object.freeze({\n    CLEAN: 1000,\n    GOING_AWAY: 1001,\n    PROTOCOL_ERROR: 1002,\n    INCORRECT_DATA: 1003,\n    ABNORMAL_CLOSURE: 1006,\n    INCONSISTENT_DATA: 1007,\n    MESSAGE_VIOLATING_POLICY: 1008,\n    MESSAGE_TOO_BIG: 1009,\n    EXTENSION_NEGOTIATION_FAILED: 1010,\n    SERVER_ERROR: 1011,\n    RESTART: 1012,\n    TRY_LATER: 1013,\n    BAD_GATEWAY: 1014,\n    SESSION_EXPIRED: 4001,\n    KEEP_ALIVE_TIMEOUT: 4002,\n    RECONNECTING: 4003,\n});\nexport const WORKER_STATE = Object.freeze({\n    CONNECTED: \"CONNECTED\",\n    DISCONNECTED: \"DISCONNECTED\",\n    IDLE: \"IDLE\",\n    CONNECTING: \"CONNECTING\",\n});\nconst MAXIMUM_RECONNECT_DELAY = 60000;\n\n/**\n * This class regroups the logic necessary in order for the\n * SharedWorker/Worker to work. Indeed, Safari and some minor browsers\n * do not support SharedWorker. In order to solve this issue, a Worker\n * is used in this case. The logic is almost the same than the one used\n * for SharedWorker and this class implements it.\n */\nexport class WebsocketWorker {\n    INITIAL_RECONNECT_DELAY = 1000;\n    RECONNECT_JITTER = 1000;\n\n    constructor() {\n        // Timestamp of start of most recent bus service sender\n        this.newestStartTs = undefined;\n        this.websocketURL = \"\";\n        this.currentUID = null;\n        this.currentDB = null;\n        this.isWaitingForNewUID = true;\n        this.channelsByClient = new Map();\n        this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY;\n        this.connectTimeout = null;\n        this.debugModeByClient = new Map();\n        this.isDebug = false;\n        this.active = true;\n        this.state = WORKER_STATE.IDLE;\n        this.isReconnecting = false;\n        this.lastChannelSubscription = null;\n        this.firstSubscribeDeferred = new Deferred();\n        this.lastNotificationId = 0;\n        this.messageWaitQueue = [];\n        this._forceUpdateChannels = debounce(this._forceUpdateChannels, 300);\n        this._debouncedUpdateChannels = debounce(this._updateChannels, 300);\n        this._debouncedSendToServer = debounce(this._sendToServer, 300);\n\n        this._onWebsocketClose = this._onWebsocketClose.bind(this);\n        this._onWebsocketError = this._onWebsocketError.bind(this);\n        this._onWebsocketMessage = this._onWebsocketMessage.bind(this);\n        this._onWebsocketOpen = this._onWebsocketOpen.bind(this);\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Send the message to all the clients that are connected to the\n     * worker.\n     *\n     * @param {WorkerEvent} type Event to broadcast to connected\n     * clients.\n     * @param {Object} data\n     */\n    broadcast(type, data) {\n        for (const client of this.channelsByClient.keys()) {\n            client.postMessage({ type, data: data ? JSON.parse(JSON.stringify(data)) : undefined });\n        }\n    }\n\n    /**\n     * Register a client handled by this worker.\n     *\n     * @param {MessagePort} messagePort\n     */\n    registerClient(messagePort) {\n        messagePort.onmessage = (ev) => {\n            this._onClientMessage(messagePort, ev.data);\n        };\n        this.channelsByClient.set(messagePort, []);\n    }\n\n    /**\n     * Send message to the given client.\n     *\n     * @param {number} client\n     * @param {WorkerEvent} type\n     * @param {Object} data\n     */\n    sendToClient(client, type, data) {\n        client.postMessage({ type, data: data ? JSON.parse(JSON.stringify(data)) : undefined });\n    }\n\n    //--------------------------------------------------------------------------\n    // PRIVATE\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when a message is posted to the worker by a client (i.e. a\n     * MessagePort connected to this worker).\n     *\n     * @param {MessagePort} client\n     * @param {Object} message\n     * @param {WorkerAction} [message.action]\n     * Action to execute.\n     * @param {Object|undefined} [message.data] Data required by the\n     * action.\n     */\n    _onClientMessage(client, { action, data }) {\n        switch (action) {\n            case \"send\": {\n                if (data[\"event_name\"] === \"update_presence\") {\n                    this._debouncedSendToServer(data);\n                } else {\n                    this._sendToServer(data);\n                }\n                return;\n            }\n            case \"start\":\n                return this._start();\n            case \"stop\":\n                return this._stop();\n            case \"leave\":\n                return this._unregisterClient(client);\n            case \"add_channel\":\n                return this._addChannel(client, data);\n            case \"delete_channel\":\n                return this._deleteChannel(client, data);\n            case \"force_update_channels\":\n                return this._forceUpdateChannels();\n            case \"initialize_connection\":\n                return this._initializeConnection(client, data);\n        }\n    }\n\n    /**\n     * Add a channel for the given client. If this channel is not yet\n     * known, update the subscription on the server.\n     *\n     * @param {MessagePort} client\n     * @param {string} channel\n     */\n    _addChannel(client, channel) {\n        const clientChannels = this.channelsByClient.get(client);\n        if (!clientChannels.includes(channel)) {\n            clientChannels.push(channel);\n            this.channelsByClient.set(client, clientChannels);\n            this._debouncedUpdateChannels();\n        }\n    }\n\n    /**\n     * Remove a channel for the given client. If this channel is not\n     * used anymore, update the subscription on the server.\n     *\n     * @param {MessagePort} client\n     * @param {string} channel\n     */\n    _deleteChannel(client, channel) {\n        const clientChannels = this.channelsByClient.get(client);\n        if (!clientChannels) {\n            return;\n        }\n        const channelIndex = clientChannels.indexOf(channel);\n        if (channelIndex !== -1) {\n            clientChannels.splice(channelIndex, 1);\n            this._debouncedUpdateChannels();\n        }\n    }\n\n    /**\n     * Update the channels on the server side even if the channels on\n     * the client side are the same than the last time we subscribed.\n     */\n    _forceUpdateChannels() {\n        this._updateChannels({ force: true });\n    }\n\n    /**\n     * Remove the given client from this worker client list as well as\n     * its channels. If some of its channels are not used anymore,\n     * update the subscription on the server.\n     *\n     * @param {MessagePort} client\n     */\n    _unregisterClient(client) {\n        this.channelsByClient.delete(client);\n        this.debugModeByClient.delete(client);\n        this.isDebug = Object.values(this.debugModeByClient).some(\n            (debugValue) => debugValue !== \"\"\n        );\n        this._debouncedUpdateChannels();\n    }\n\n    /**\n     * Initialize a client connection to this worker.\n     *\n     * @param {Object} param0\n     * @param {string} [param0.db] Database name.\n     * @param {String} [param0.debug] Current debugging mode for the\n     * given client.\n     * @param {Number} [param0.lastNotificationId] Last notification id\n     * known by the client.\n     * @param {String} [param0.websocketURL] URL of the websocket endpoint.\n     * @param {Number|false|undefined} [param0.uid] Current user id\n     *     - Number: user is logged whether on the frontend/backend.\n     *     - false: user is not logged.\n     *     - undefined: not available (e.g. livechat support page)\n     * @param {Number} param0.startTs Timestamp of start of bus service sender.\n     */\n    _initializeConnection(client, { db, debug, lastNotificationId, uid, websocketURL, startTs }) {\n        if (this.newestStartTs && this.newestStartTs > startTs) {\n            this.debugModeByClient[client] = debug;\n            this.isDebug = Object.values(this.debugModeByClient).some(\n                (debugValue) => debugValue !== \"\"\n            );\n            this.sendToClient(client, \"initialized\");\n            this.sendToClient(client, \"update_state\", this.state);\n            return;\n        }\n        this.newestStartTs = startTs;\n        this.websocketURL = websocketURL;\n        this.lastNotificationId = lastNotificationId;\n        this.debugModeByClient[client] = debug;\n        this.isDebug = Object.values(this.debugModeByClient).some(\n            (debugValue) => debugValue !== \"\"\n        );\n        const isCurrentUserKnown = uid !== undefined;\n        if (this.isWaitingForNewUID && isCurrentUserKnown) {\n            this.isWaitingForNewUID = false;\n            this.currentUID = uid;\n        }\n        if ((this.currentUID !== uid && isCurrentUserKnown) || this.currentDB !== db) {\n            this.currentUID = uid;\n            this.currentDB = db;\n            if (this.websocket) {\n                this.websocket.close(WEBSOCKET_CLOSE_CODES.CLEAN);\n            }\n            this.channelsByClient.forEach((_, key) => this.channelsByClient.set(key, []));\n        }\n        this.sendToClient(client, \"initialized\");\n        this.sendToClient(client, \"update_state\", this.state);\n        if (!this.active) {\n            this.sendToClient(client, \"outdated\");\n        }\n    }\n\n    /**\n     * Determine whether or not the websocket associated to this worker\n     * is connected.\n     *\n     * @returns {boolean}\n     */\n    _isWebsocketConnected() {\n        return this.websocket && this.websocket.readyState === 1;\n    }\n\n    /**\n     * Determine whether or not the websocket associated to this worker\n     * is connecting.\n     *\n     * @returns {boolean}\n     */\n    _isWebsocketConnecting() {\n        return this.websocket && this.websocket.readyState === 0;\n    }\n\n    /**\n     * Determine whether or not the websocket associated to this worker\n     * is in the closing state.\n     *\n     * @returns {boolean}\n     */\n    _isWebsocketClosing() {\n        return this.websocket && this.websocket.readyState === 2;\n    }\n\n    /**\n     * Triggered when a connection is closed. If closure was not clean ,\n     * try to reconnect after indicating to the clients that the\n     * connection was closed.\n     *\n     * @param {CloseEvent} ev\n     * @param {number} code  close code indicating why the connection\n     * was closed.\n     * @param {string} reason reason indicating why the connection was\n     * closed.\n     */\n    _onWebsocketClose({ code, reason }) {\n        this._updateState(WORKER_STATE.DISCONNECTED);\n        if (this.isDebug) {\n            console.debug(\n                `%c${new Date().toLocaleString()} - [onClose]`,\n                \"color: #c6e; font-weight: bold;\",\n                code,\n                reason\n            );\n        }\n        this.lastChannelSubscription = null;\n        this.firstSubscribeDeferred = new Deferred();\n        if (this.isReconnecting) {\n            // Connection was not established but the close event was\n            // triggered anyway. Let the onWebsocketError method handle\n            // this case.\n            return;\n        }\n        this.broadcast(\"disconnect\", { code, reason });\n        if (code === WEBSOCKET_CLOSE_CODES.CLEAN) {\n            if (reason === \"OUTDATED_VERSION\") {\n                console.warn(\"Worker deactivated due to an outdated version.\");\n                this.active = false;\n                this.broadcast(\"outdated\");\n            }\n            // WebSocket was closed on purpose, do not try to reconnect.\n            return;\n        }\n        // WebSocket was not closed cleanly, let's try to reconnect.\n        this.broadcast(\"reconnecting\", { closeCode: code });\n        this.isReconnecting = true;\n        if (code === WEBSOCKET_CLOSE_CODES.KEEP_ALIVE_TIMEOUT) {\n            // Don't wait to reconnect on keep alive timeout.\n            this.connectRetryDelay = 0;\n        }\n        if (code === WEBSOCKET_CLOSE_CODES.SESSION_EXPIRED) {\n            this.isWaitingForNewUID = true;\n        }\n        this._retryConnectionWithDelay();\n    }\n\n    /**\n     * Triggered when a connection failed or failed to established.\n     */\n    _onWebsocketError() {\n        if (this.isDebug) {\n            console.debug(\n                `%c${new Date().toLocaleString()} - [onError]`,\n                \"color: #c6e; font-weight: bold;\"\n            );\n        }\n        this._retryConnectionWithDelay();\n    }\n\n    /**\n     * Handle data received from the bus.\n     *\n     * @param {MessageEvent} messageEv\n     */\n    _onWebsocketMessage(messageEv) {\n        const notifications = JSON.parse(messageEv.data);\n        if (this.isDebug) {\n            console.debug(\n                `%c${new Date().toLocaleString()} - [onMessage]`,\n                \"color: #c6e; font-weight: bold;\",\n                notifications\n            );\n        }\n        this.lastNotificationId = notifications[notifications.length - 1].id;\n        this.broadcast(\"notification\", notifications);\n    }\n\n    /**\n     * Triggered on websocket open. Send message that were waiting for\n     * the connection to open.\n     */\n    _onWebsocketOpen() {\n        this._updateState(WORKER_STATE.CONNECTED);\n        if (this.isDebug) {\n            console.debug(\n                `%c${new Date().toLocaleString()} - [onOpen]`,\n                \"color: #c6e; font-weight: bold;\"\n            );\n        }\n        this.broadcast(this.isReconnecting ? \"reconnect\" : \"connect\");\n        this._debouncedUpdateChannels();\n        this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY;\n        this.connectTimeout = null;\n        this.isReconnecting = false;\n        this.firstSubscribeDeferred.then(() => {\n            this.messageWaitQueue.forEach((msg) => this.websocket.send(msg));\n            this.messageWaitQueue = [];\n        });\n    }\n\n    /**\n     * Try to reconnect to the server, an exponential back off is\n     * applied to the reconnect attempts.\n     */\n    _retryConnectionWithDelay() {\n        this.connectRetryDelay =\n            Math.min(this.connectRetryDelay * 1.5, MAXIMUM_RECONNECT_DELAY) +\n            this.RECONNECT_JITTER * Math.random();\n        this.connectTimeout = setTimeout(this._start.bind(this), this.connectRetryDelay);\n    }\n\n    /**\n     * Send a message to the server through the websocket connection.\n     * If the websocket is not open, enqueue the message and send it\n     * upon the next reconnection.\n     *\n     * @param {{event_name: string, data: any }} message Message to send to the server.\n     */\n    _sendToServer(message) {\n        const payload = JSON.stringify(message);\n        if (!this._isWebsocketConnected()) {\n            if (message[\"event_name\"] === \"subscribe\") {\n                this.messageWaitQueue = this.messageWaitQueue.filter(\n                    (msg) => JSON.parse(msg).event_name !== \"subscribe\"\n                );\n                this.messageWaitQueue.unshift(payload);\n            } else {\n                this.messageWaitQueue.push(payload);\n            }\n        } else {\n            if (message[\"event_name\"] === \"subscribe\") {\n                this.websocket.send(payload);\n            } else {\n                this.firstSubscribeDeferred.then(() => this.websocket.send(payload));\n            }\n        }\n    }\n\n    /**\n     * Start the worker by opening a websocket connection.\n     */\n    _start() {\n        if (!this.active || this._isWebsocketConnected() || this._isWebsocketConnecting()) {\n            return;\n        }\n        if (this.websocket) {\n            this.websocket.removeEventListener(\"open\", this._onWebsocketOpen);\n            this.websocket.removeEventListener(\"message\", this._onWebsocketMessage);\n            this.websocket.removeEventListener(\"error\", this._onWebsocketError);\n            this.websocket.removeEventListener(\"close\", this._onWebsocketClose);\n        }\n        if (this._isWebsocketClosing()) {\n            // close event was not triggered and will never be, broadcast the\n            // disconnect event for consistency sake.\n            this.lastChannelSubscription = null;\n            this.broadcast(\"disconnect\", { code: WEBSOCKET_CLOSE_CODES.ABNORMAL_CLOSURE });\n        }\n        this._updateState(WORKER_STATE.CONNECTING);\n        this.websocket = new WebSocket(this.websocketURL);\n        this.websocket.addEventListener(\"open\", this._onWebsocketOpen);\n        this.websocket.addEventListener(\"error\", this._onWebsocketError);\n        this.websocket.addEventListener(\"message\", this._onWebsocketMessage);\n        this.websocket.addEventListener(\"close\", this._onWebsocketClose);\n    }\n\n    /**\n     * Stop the worker.\n     */\n    _stop() {\n        clearTimeout(this.connectTimeout);\n        this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY;\n        this.isReconnecting = false;\n        this.lastChannelSubscription = null;\n        if (this.websocket) {\n            this.websocket.close();\n        }\n    }\n\n    /**\n     * Update the channel subscription on the server. Ignore if the channels\n     * did not change since the last subscription.\n     *\n     * @param {boolean} force Whether or not we should update the subscription\n     * event if the channels haven't change since last subscription.\n     */\n    _updateChannels({ force = false } = {}) {\n        const allTabsChannels = [\n            ...new Set([].concat.apply([], [...this.channelsByClient.values()])),\n        ].sort();\n        const allTabsChannelsString = JSON.stringify(allTabsChannels);\n        const shouldUpdateChannelSubscription =\n            allTabsChannelsString !== this.lastChannelSubscription;\n        if (force || shouldUpdateChannelSubscription) {\n            this.lastChannelSubscription = allTabsChannelsString;\n            this._sendToServer({\n                event_name: \"subscribe\",\n                data: { channels: allTabsChannels, last: this.lastNotificationId },\n            });\n            this.firstSubscribeDeferred.resolve();\n        }\n    }\n    /**\n     * Update the worker state and broadcast the new state to its clients.\n     *\n     * @param {WORKER_STATE[keyof WORKER_STATE]} newState\n     */\n    _updateState(newState) {\n        this.state = newState;\n        this.broadcast(\"update_state\", newState);\n    }\n}\n", "/** @odoo-module **/\n\n/**\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing.\n *\n * Inspired by https://davidwalsh.name/javascript-debounce-function\n */\nexport function debounce(func, wait, immediate) {\n    let timeout;\n    return function () {\n        const context = this;\n        const args = arguments;\n        function later() {\n            timeout = null;\n            if (!immediate) {\n                func.apply(context, args);\n            }\n        }\n        const callNow = immediate && !timeout;\n        clearTimeout(timeout);\n        timeout = setTimeout(later, wait);\n        if (callNow) {\n            func.apply(context, args);\n        }\n    };\n}\n\n/**\n * Deferred is basically a resolvable/rejectable extension of Promise.\n */\nexport class Deferred extends Promise {\n    constructor() {\n        let resolve;\n        let reject;\n        const prom = new Promise((res, rej) => {\n            resolve = res;\n            reject = rej;\n        });\n        return Object.assign(prom, { resolve, reject });\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, useEffect, useRef } from \"@odoo/owl\";\nimport { usePosition } from \"@web/core/position/position_hook\";\n\n/**\n * @typedef {import(\"../tour_service/tour_pointer_state\").TourPointerState} TourPointerState\n *\n * @typedef TourPointerProps\n * @property {TourPointerState} pointerState\n * @property {boolean} bounce\n */\n\n/** @extends {Component<TourPointerProps, any>} */\nexport class TourPointer extends Component {\n    static props = {\n        pointerState: {\n            type: Object,\n            shape: {\n                anchor: { type: HTMLElement, optional: true },\n                content: { type: String, optional: true },\n                isOpen: { type: Boolean, optional: true },\n                isVisible: { type: Boolean, optional: true },\n                isZone: { type: Boolean, optional: true },\n                onClick: { type: [Function, { value: null }], optional: true },\n                onMouseEnter: { type: [Function, { value: null }], optional: true },\n                onMouseLeave: { type: [Function, { value: null }], optional: true },\n                position: {\n                    type: [\n                        { value: \"left\" },\n                        { value: \"right\" },\n                        { value: \"top\" },\n                        { value: \"bottom\" },\n                    ],\n                    optional: true,\n                },\n                rev: { type: Number, optional: true },\n            },\n        },\n        bounce: { type: Boolean, optional: true },\n    };\n\n    static defaultProps = {\n        bounce: true,\n    };\n\n    static template = \"web_tour.TourPointer\";\n    static width = 28; // in pixels\n    static height = 28; // in pixels\n\n    setup() {\n        const positionOptions = {\n            margin: 6,\n            onPositioned: (pointer, position) => {\n                const popperRect = pointer.getBoundingClientRect();\n                const { top, left, direction } = position;\n                if (direction === \"top\") {\n                    // position from the bottom instead of the top as it is needed\n                    // to ensure the expand animation is properly done\n                    pointer.style.bottom = `${window.innerHeight - top - popperRect.height}px`;\n                    pointer.style.removeProperty(\"top\");\n                } else if (direction === \"left\") {\n                    // position from the right instead of the left as it is needed\n                    // to ensure the expand animation is properly done\n                    pointer.style.right = `${window.innerWidth - left - popperRect.width}px`;\n                    pointer.style.removeProperty(\"left\");\n                }\n            },\n        };\n        Object.defineProperty(positionOptions, \"position\", {\n            get: () => this.position,\n            enumerable: true,\n        });\n        const position = usePosition(\n            \"pointer\",\n            () => this.props.pointerState.anchor,\n            positionOptions\n        );\n        const rootRef = useRef(\"pointer\");\n        const zoneRef = useRef(\"zone\");\n        /** @type {DOMREct | null} */\n        let dimensions = null;\n        let lastMeasuredContent = null;\n        let lastOpenState = this.isOpen;\n        let lastAnchor;\n        let [anchorX, anchorY] = [0, 0];\n        useEffect(() => {\n            const { el: pointer } = rootRef;\n            const { el: zone } = zoneRef;\n            if (pointer) {\n                const hasContentChanged = lastMeasuredContent !== this.content;\n                const hasOpenStateChanged = lastOpenState !== this.isOpen;\n                lastOpenState = this.isOpen;\n\n                // Check is the pointed element is a zone\n                if (this.props.pointerState.isZone) {\n                    const { anchor } = this.props.pointerState;\n                    const { left, top, width, height } = anchor.getBoundingClientRect();\n                    zone.style.minWidth = width + \"px\";\n                    zone.style.minHeight = height + \"px\";\n                    zone.style.left = left + \"px\";\n                    zone.style.top = top + \"px\";\n                }\n\n                // Content changed: we must re-measure the dimensions of the text.\n                if (hasContentChanged) {\n                    lastMeasuredContent = this.content;\n                    pointer.style.removeProperty(\"width\");\n                    pointer.style.removeProperty(\"height\");\n                    dimensions = pointer.getBoundingClientRect();\n                }\n\n                // If the content or the \"is open\" state changed: we must apply\n                // new width and height properties\n                if (hasContentChanged || hasOpenStateChanged) {\n                    const [width, height] = this.isOpen\n                        ? [dimensions.width, dimensions.height]\n                        : [this.constructor.width, this.constructor.height];\n                    if (this.isOpen) {\n                        pointer.style.removeProperty(\"transition\");\n                    } else {\n                        // No transition if switching from open to closed\n                        pointer.style.setProperty(\"transition\", \"none\");\n                    }\n                    pointer.style.setProperty(\"width\", `${width}px`);\n                    pointer.style.setProperty(\"height\", `${height}px`);\n                }\n\n                if (!this.isOpen) {\n                    const { anchor } = this.props.pointerState;\n                    if (anchor === lastAnchor) {\n                        const { x, y, width } = anchor.getBoundingClientRect();\n                        const [lastAnchorX, lastAnchorY] = [anchorX, anchorY];\n                        [anchorX, anchorY] = [x, y];\n                        // Let's just say that the anchor is static if it moved less than 1px.\n                        const delta = Math.sqrt(\n                            Math.pow(x - lastAnchorX, 2) + Math.pow(y - lastAnchorY, 2)\n                        );\n                        if (delta < 1) {\n                            position.lock();\n                            return;\n                        }\n                        const wouldOverflow = window.innerWidth - x - width / 2 < dimensions?.width;\n                        pointer.classList.toggle(\"o_expand_left\", wouldOverflow);\n                    }\n                    lastAnchor = anchor;\n                    pointer.style.bottom = \"\";\n                    pointer.style.right = \"\";\n                    position.unlock();\n                }\n            } else {\n                lastMeasuredContent = null;\n                lastOpenState = false;\n                lastAnchor = null;\n                dimensions = null;\n            }\n        });\n    }\n\n    get content() {\n        return this.props.pointerState.content || \"\";\n    }\n\n    get isOpen() {\n        return this.props.pointerState.isOpen && this.content;\n    }\n\n    get position() {\n        return this.props.pointerState.position || \"top\";\n    }\n}\n", "import { tourState } from \"./tour_state\";\nimport { config as transitionConfig } from \"@web/core/transition\";\nimport { TourStepAutomatic } from \"./tour_step_automatic\";\nimport { MacroEngine } from \"@web/core/macro\";\n\nexport class TourAutomatic {\n    mode = \"auto\";\n    constructor(data) {\n        Object.assign(this, data);\n        this.steps = this.steps.map((step, index) => new TourStepAutomatic(step, this, index));\n        this.macroEngine = new MacroEngine({\n            target: document,\n            defaultCheckDelay: 500,\n        });\n        const tourConfig = tourState.getCurrentConfig();\n        this.stepDelay = tourConfig.stepDelay;\n    }\n\n    start(pointer, callback) {\n        const currentStepIndex = tourState.getCurrentIndex();\n        const macroSteps = this.steps\n            .filter((step) => step.index >= currentStepIndex)\n            .flatMap((step) => step.compileToMacro(pointer))\n            .concat([\n                {\n                    action: () => {\n                        if (tourState.getCurrentTourOnError()) {\n                            console.error(\"tour not succeeded\");\n                        } else {\n                            transitionConfig.disabled = false;\n                            callback();\n                        }\n                    },\n                },\n            ]);\n\n        const macro = {\n            name: this.name,\n            checkDelay: this.checkDelay,\n            steps: macroSteps,\n        };\n\n        transitionConfig.disabled = true;\n        //Activate macro in exclusive mode (only one macro per MacroEngine)\n        this.macroEngine.activate(macro, true);\n    }\n}\n", "import * as hoot from \"@odoo/hoot-dom\";\n\nexport class TourHelpers {\n    /**\n     * @typedef {string|Node} Selector\n     */\n\n    constructor(anchor) {\n        this.anchor = anchor;\n        this.delay = 20;\n    }\n\n    /**\n     * Ensures that the given {@link Selector} is checked.\n     * @description\n     * If it is not checked, a click is triggered on the input.\n     * If the input is still not checked after the click, an error is thrown.\n     *\n     * @param {string|Node} selector\n     * @example\n     *  run: \"check\", //Checks the action element\n     * @example\n     *  run: \"check input[type=checkbox]\", // Checks the selector\n     */\n    async check(selector) {\n        const element = this._get_action_element(selector);\n        this._ensureEnabled(element, \"check\");\n        await hoot.check(element);\n    }\n\n    /**\n     * Clears the **value** of the **{@link Selector}**.\n     * @description\n     * This is done using the following sequence:\n     * - pressing \"Control\" + \"A\" to select the whole value;\n     * - pressing \"Backspace\" to delete the value;\n     * - (optional) triggering a \"change\" event by pressing \"Enter\".\n     *\n     * @param {Selector} selector\n     * @example\n     *  run: \"clear\", // Clears the value of the action element\n     * @example\n     *  run: \"clear input#my_input\", // Clears the value of the selector\n     */\n    async clear(selector) {\n        const element = this._get_action_element(selector);\n        this._ensureEnabled(element, \"clear\");\n        await hoot.click(element);\n        await hoot.clear();\n    }\n\n    /**\n     * Performs a click sequence on the given **{@link Selector}**\n     * @description Let's see more informations about click sequence here: {@link hoot.click}\n     * @param {Selector} selector\n     * @example\n     *  run: \"click\", // Click on the action element\n     * @example\n     *  run: \"click .o_rows:first\", // Click on the selector\n     */\n    async click(selector) {\n        const element = this._get_action_element(selector);\n        this._ensureEnabled(element, \"click\");\n        await hoot.click(element);\n    }\n\n    /**\n     * Performs two click sequences on the given **{@link Selector}**.\n     * @description Let's see more informations about click sequence here: {@link hoot.dblclick}\n     * @param {Selector} selector\n     * @example\n     *  run: \"dblclick\", // Double click on the action element\n     * @example\n     *  run: \"dblclick .o_rows:first\", // Double click on the selector\n     */\n    async dblclick(selector) {\n        const element = this._get_action_element(selector);\n        this._ensureEnabled(element, \"dblclick\");\n        await hoot.dblclick(element);\n    }\n\n    /**\n     * Starts a drag sequence on the active element (anchor) and drop it on the given **{@link Selector}**.\n     * @param {Selector} selector\n     * @param {hoot.PointerOptions} options\n     * @example\n     *  run: \"drag_and_drop .o_rows:first\", // Drag the active element and drop it in the selector\n     * @example\n     *  async run(helpers) {\n     *      await helpers.drag_and_drop(\".o_rows:first\", {\n     *          position: {\n     *              top: 40,\n     *              left: 5,\n     *          },\n     *          relative: true,\n     *      });\n     *  }\n     */\n    async drag_and_drop(selector, options) {\n        if (typeof options !== \"object\") {\n            options = { position: \"top\", relative: true };\n        }\n        const dragEffectDelay = async () => {\n            await new Promise((resolve) => requestAnimationFrame(resolve));\n            await new Promise((resolve) => setTimeout(resolve, this.delay));\n        };\n        const element = this.anchor;\n        this._ensureEnabled(element, \"drag and drop\");\n        const { drop, moveTo } = await hoot.drag(element);\n        await dragEffectDelay();\n        await hoot.hover(element, {\n            position: {\n                top: 20,\n                left: 20,\n            },\n            relative: true,\n        });\n        await dragEffectDelay();\n        const target = await hoot.waitFor(selector, {\n            visible: true,\n            timeout: 500,\n        });\n        await moveTo(target, options);\n        await dragEffectDelay();\n        await drop();\n        await dragEffectDelay();\n    }\n\n    /**\n     * Edit input or textarea given by **{@link selector}**\n     * @param {string} text\n     * @param {Selector} selector\n     * @example\n     *  run: \"edit Hello Mr. Doku\",\n     */\n    async edit(text, selector) {\n        const element = this._get_action_element(selector);\n        await hoot.click(element);\n        await hoot.edit(text);\n    }\n\n    /**\n     * Edit only editable wysiwyg element given by **{@link Selector}**\n     * @param {string} text\n     * @param {Selector} selector\n     */\n    async editor(text, selector) {\n        const element = this._get_action_element(selector);\n        const InEditor = Boolean(element.closest(\".odoo-editor-editable\"));\n        if (!InEditor) {\n            throw new Error(\"run 'editor' always on an element in an editor\");\n        }\n        this._ensureEnabled(element, \"edit wysiwyg\");\n        await hoot.click(element);\n        this._set_range(element, \"start\");\n        await hoot.keyDown(\"_\");\n        element.textContent = text;\n        await hoot.manuallyDispatchProgrammaticEvent(element, \"input\");\n        this._set_range(element, \"stop\");\n        await hoot.keyUp(\"_\");\n        await hoot.manuallyDispatchProgrammaticEvent(element, \"change\");\n    }\n\n    /**\n     * Fills the **{@link Selector}** with the given `value`.\n     * @description This helper is intended for `<input>` and `<textarea>` elements,\n     * with the exception of `\"checkbox\"` and `\"radio\"` types, which should be\n     * selected using the {@link check} helper.\n     * In tour, it's mainly usefull for autocomplete components.\n     * @param {string} value\n     * @param {Selector} selector\n     */\n    async fill(value, selector) {\n        const element = this._get_action_element(selector);\n        await hoot.click(element);\n        await hoot.fill(value);\n    }\n\n    /**\n     * Performs a hover sequence on the given **{@link Selector}**.\n     * @param {Selector} selector\n     * @example\n     *  run: \"hover\",\n     */\n    async hover(selector) {\n        const element = this._get_action_element(selector);\n        await hoot.hover(element);\n    }\n\n    /**\n     * Only for input[type=\"range\"]\n     * @param {string|number} value\n     * @param {Selector} selector\n     */\n    async range(value, selector) {\n        const element = this._get_action_element(selector);\n        this._ensureEnabled(element, \"range\");\n        await hoot.click(element);\n        await hoot.setInputRange(element, value);\n    }\n\n    /**\n     * Performs a keyboard event sequence.\n     * @example\n     *  run : \"press Enter\",\n     */\n    press(...args) {\n        return hoot.press(args.flatMap((arg) => typeof arg === \"string\" && arg.split(\"+\")));\n    }\n\n    /**\n     * Performs a selection event sequence on **{@link Selector}**. This helper is intended\n     * for `<select>` elements only.\n     * @description Select the option by its value\n     * @param {string} value\n     * @param {Selector} selector\n     * @example\n     * run(helpers) => {\n     *  helpers.select(\"Kevin17\", \"select#mySelect\");\n     * },\n     * @example\n     * run: \"select Foden47\",\n     */\n    async select(value, selector) {\n        const element = this._get_action_element(selector);\n        this._ensureEnabled(element, \"select\");\n        await hoot.click(element);\n        await hoot.select(value, { target: element });\n    }\n\n    /**\n     * Performs a selection event sequence on **{@link Selector}**\n     * @description Select the option by its index\n     * @param {number} index starts at 0\n     * @param {Selector} selector\n     * @example\n     *  run: \"selectByIndex 2\", //Select the third option\n     */\n    async selectByIndex(index, selector) {\n        const element = this._get_action_element(selector);\n        this._ensureEnabled(element, \"selectByIndex\");\n        await hoot.click(element);\n        const value = hoot.queryValue(`option:eq(${index})`, { root: element });\n        if (value) {\n            await hoot.select(value, { target: element });\n            await hoot.manuallyDispatchProgrammaticEvent(element, \"input\");\n        }\n    }\n\n    /**\n     * Performs a selection event sequence on **{@link Selector}**\n     * @description Select option(s) by there labels\n     * @param {string|RegExp} contains\n     * @param {Selector} selector\n     * @example\n     *  run: \"selectByLabel Jeremy Doku\", //Select all options where label contains Jeremy Doku\n     */\n    async selectByLabel(contains, selector) {\n        const element = this._get_action_element(selector);\n        this._ensureEnabled(element, \"selectByLabel\");\n        await hoot.click(element);\n        const values = hoot.queryAllValues(`option:contains(${contains})`, { root: element });\n        await hoot.select(values, { target: element });\n    }\n\n    /**\n     * Ensures that the given {@link Selector} is unchecked.\n     * @description\n     * If it is checked, a click is triggered on the input.\n     * If the input is still checked after the click, an error is thrown.\n     *\n     * @param {string|Node} selector\n     * @example\n     *  run: \"uncheck\", // Unchecks the action element\n     * @example\n     *  run: \"uncheck input[type=checkbox]\", // Unchecks the selector\n     */\n    uncheck(selector) {\n        const element = this._get_action_element(selector);\n        this._ensureEnabled(element, \"uncheck\");\n        hoot.uncheck(element);\n    }\n\n    /**\n     * Navigate to {@link url}.\n     *\n     * @param {string} url\n     * @example\n     *  run: \"goToUrl /shop\", // Go to /shop\n     */\n    goToUrl(url) {\n        const linkEl = document.createElement(\"a\");\n        linkEl.href = url;\n        linkEl.click();\n    }\n\n    /**\n     * Get Node for **{@link Selector}**\n     * @param {Selector} selector\n     * @returns {Node}\n     * @default this.anchor\n     */\n    _get_action_element(selector) {\n        if (typeof selector === \"string\" && selector.length) {\n            const nodes = hoot.queryAll(selector);\n            return nodes.find(hoot.isVisible) || nodes.at(0);\n        } else if (typeof selector === \"object\" && Boolean(selector?.nodeType)) {\n            return selector;\n        }\n        return this.anchor;\n    }\n\n    // Useful for wysiwyg editor.\n    _set_range(element, start_or_stop) {\n        function _node_length(node) {\n            if (node.nodeType === Node.TEXT_NODE) {\n                return node.nodeValue.length;\n            } else {\n                return node.childNodes.length;\n            }\n        }\n        const selection = element.ownerDocument.getSelection();\n        selection.removeAllRanges();\n        const range = new Range();\n        let node = element;\n        let length = 0;\n        if (start_or_stop === \"start\") {\n            while (node.firstChild) {\n                node = node.firstChild;\n            }\n        } else {\n            while (node.lastChild) {\n                node = node.lastChild;\n            }\n            length = _node_length(node);\n        }\n        range.setStart(node, length);\n        range.setEnd(node, length);\n        selection.addRange(range);\n    }\n\n    /**\n     * Return true when element is not disabled\n     * @param {Node} element\n     */\n    _ensureEnabled(element, action = \"do action\") {\n        if (element.disabled) {\n            throw new Error(\n                `Element can't be disabled when you want to ${action} on it.\nTip: You can add the \":enabled\" pseudo selector to your selector to wait for the element is enabled.`\n            );\n        }\n    }\n}\n", "import { tourState } from \"@web_tour/tour_service/tour_state\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { getScrollParent } from \"@web_tour/tour_service/tour_utils\";\nimport * as hoot from \"@odoo/hoot-dom\";\nimport { utils } from \"@web/core/ui/ui_service\";\nimport { TourStep } from \"./tour_step\";\nimport { MacroMutationObserver } from \"@web/core/macro\";\n\n/**\n * @typedef ConsumeEvent\n * @property {string} name\n * @property {Element} target\n * @property {(ev: Event) => boolean} conditional\n */\n\nexport class TourInteractive {\n    mode = \"manual\";\n    currentAction;\n    currentActionIndex;\n    anchorEls = [];\n    removeListeners = () => {};\n\n    /**\n     * @param {Tour} data\n     */\n    constructor(data) {\n        Object.assign(this, data);\n        this.steps = this.steps.map((step) => new TourStep(step, this));\n        this.actions = this.steps.flatMap((s) => this.getSubActions(s));\n    }\n\n    /**\n     * @param {import(\"@web_tour/tour_pointer/tour_pointer\").TourPointer} pointer\n     * @param {Function} onTourEnd\n     */\n    start(pointer, onTourEnd) {\n        this.pointer = pointer;\n        this.debouncedToggleOpen = debounce(this.pointer.showContent, 50, true);\n        this.onTourEnd = onTourEnd;\n        this.observer = new MacroMutationObserver(() => this._onMutation());\n        this.observer.observe(document.body);\n        this.currentActionIndex = tourState.getCurrentIndex();\n        this.play();\n    }\n\n    backward() {\n        let tempIndex = this.currentActionIndex;\n        let tempAction,\n            tempAnchors = [];\n        while (!tempAnchors.length && tempIndex >= 0) {\n            tempIndex--;\n            tempAction = this.actions.at(tempIndex);\n            if (!tempAction.step.active) {\n                continue;\n            }\n            tempAnchors = tempAction && this.findTriggers(tempAction.anchor);\n        }\n\n        if (tempIndex >= 0) {\n            this.currentActionIndex = tempIndex;\n            this.play();\n        }\n    }\n\n    /**\n     * @returns {HTMLElement[]}\n     */\n    findTriggers(anchor) {\n        if (!anchor) {\n            anchor = this.currentAction.anchor;\n        }\n\n        return anchor\n            .split(/,\\s*(?![^(]*\\))/)\n            .map((part) => hoot.queryFirst(part, { visible: true }))\n            .filter((el) => !!el)\n            .map((el) => this.getAnchorEl(el, this.currentAction.event))\n            .filter((el) => !!el);\n    }\n\n    play() {\n        this.removeListeners();\n        if (this.currentActionIndex === this.actions.length) {\n            this.observer.disconnect();\n            this.onTourEnd();\n            return;\n        }\n\n        this.currentAction = this.actions.at(this.currentActionIndex);\n\n        if (!this.currentAction.step.active || this.currentAction.event === \"warn\") {\n            if (this.currentAction.event === \"warn\") {\n                console.warn(`Step '${this.currentAction.anchor}' ignored.`);\n            }\n            this.currentActionIndex++;\n            this.play();\n            return;\n        }\n\n        console.log(this.currentAction.event, this.currentAction.anchor);\n\n        tourState.setCurrentIndex(this.currentActionIndex);\n        this.anchorEls = this.findTriggers();\n        this.setActionListeners();\n        this.updatePointer();\n    }\n\n    updatePointer() {\n        if (this.anchorEls.length) {\n            this.pointer.pointTo(\n                this.anchorEls[0],\n                this.currentAction.pointerInfo,\n                this.currentAction.event === \"drop\"\n            );\n            this.pointer.setState({\n                onMouseEnter: () => this.debouncedToggleOpen(true),\n                onMouseLeave: () => this.debouncedToggleOpen(false),\n            });\n        }\n    }\n\n    setActionListeners() {\n        const cleanups = this.anchorEls.flatMap((anchorEl, index) => {\n            const toListen = {\n                anchorEl,\n                consumeEvents: this.getConsumeEventType(anchorEl, this.currentAction.event),\n                onConsume: () => {\n                    this.pointer.hide();\n                    this.currentActionIndex++;\n                    this.play();\n                },\n                onError: () => {\n                    if (this.currentAction.event === \"drop\") {\n                        this.pointer.hide();\n                        this.currentActionIndex--;\n                        this.play();\n                    }\n                },\n            };\n            if (index === 0) {\n                return this.setupListeners({\n                    ...toListen,\n                    onMouseEnter: () => this.pointer.showContent(true),\n                    onMouseLeave: () => this.pointer.showContent(false),\n                    onScroll: () => this.updatePointer(),\n                });\n            } else {\n                return this.setupListeners({\n                    ...toListen,\n                    onScroll: () => {},\n                });\n            }\n        });\n        this.removeListeners = () => {\n            this.anchorEls = [];\n            while (cleanups.length) {\n                cleanups.pop()();\n            }\n        };\n    }\n\n    /**\n     * @param {HTMLElement} params.anchorEl\n     * @param {import(\"./tour_utils\").ConsumeEvent[]} params.consumeEvents\n     * @param {() => void} params.onMouseEnter\n     * @param {() => void} params.onMouseLeave\n     * @param {(ev: Event) => any} params.onScroll\n     * @param {(ev: Event) => any} params.onConsume\n     * @param {() => any} params.onError\n     */\n    setupListeners({\n        anchorEl,\n        consumeEvents,\n        onMouseEnter,\n        onMouseLeave,\n        onScroll,\n        onConsume,\n        onError = () => {},\n    }) {\n        consumeEvents = consumeEvents.map((c) => ({\n            target: c.target,\n            type: c.name,\n            listener: function (ev) {\n                if (!c.conditional || c.conditional(ev)) {\n                    onConsume();\n                } else {\n                    onError();\n                }\n            },\n        }));\n\n        for (const consume of consumeEvents) {\n            consume.target.addEventListener(consume.type, consume.listener, true);\n        }\n        anchorEl.addEventListener(\"mouseenter\", onMouseEnter);\n        anchorEl.addEventListener(\"mouseleave\", onMouseLeave);\n\n        const cleanups = [\n            () => {\n                for (const consume of consumeEvents) {\n                    consume.target.removeEventListener(consume.type, consume.listener, true);\n                }\n                anchorEl.removeEventListener(\"mouseenter\", onMouseEnter);\n                anchorEl.removeEventListener(\"mouseleave\", onMouseLeave);\n            },\n        ];\n\n        const scrollEl = getScrollParent(anchorEl);\n        if (scrollEl) {\n            const debouncedOnScroll = debounce(onScroll, 50);\n            scrollEl.addEventListener(\"scroll\", debouncedOnScroll);\n            cleanups.push(() => scrollEl.removeEventListener(\"scroll\", debouncedOnScroll));\n        }\n\n        return cleanups;\n    }\n\n    /**\n     *\n     * @param {import(\"./tour_service\").TourStep} step\n     * @returns {{\n     *  event: string,\n     *  anchor: string,\n     *  pointerInfo: { tooltipPosition: string?, content: string? },\n     * }[]}\n     */\n    getSubActions(step) {\n        const actions = [];\n        if (!step.run || typeof step.run === \"function\") {\n            actions.push({\n                step,\n                event: \"warn\",\n                anchor: step.trigger,\n            });\n            return actions;\n        }\n\n        for (const todo of step.run.split(\"&&\")) {\n            const m = String(todo)\n                .trim()\n                .match(/^(?<action>\\w*) *\\(? *(?<arguments>.*?)\\)?$/);\n\n            let action = m.groups?.action;\n            const anchor = m.groups?.arguments || step.trigger;\n            const pointerInfo = {\n                content: step.content,\n                tooltipPosition: step.tooltipPosition,\n            };\n\n            if (action === \"drag_and_drop\") {\n                actions.push({\n                    step,\n                    event: \"drag\",\n                    anchor: step.trigger,\n                    pointerInfo,\n                });\n                action = \"drop\";\n            }\n\n            actions.push({\n                step,\n                event: action,\n                anchor: action === \"edit\" ? step.trigger : anchor,\n                pointerInfo,\n            });\n        }\n\n        return actions;\n    }\n\n    /**\n     * @param {HTMLElement} [element]\n     * @param {string} [runCommand]\n     * @returns {ConsumeEvent[]}\n     */\n    getConsumeEventType(element, runCommand) {\n        const consumeEvents = [];\n        if (runCommand === \"click\") {\n            consumeEvents.push({\n                name: \"click\",\n                target: element,\n            });\n\n            // Click on a field widget with an autocomplete should be also completed with a selection though Enter or Tab\n            // This case is for the steps that click on field_widget\n            if (element.querySelector(\".o-autocomplete--input\")) {\n                consumeEvents.push({\n                    name: \"keydown\",\n                    target: element.querySelector(\".o-autocomplete--input\"),\n                    conditional: (ev) =>\n                        [\"Tab\", \"Enter\"].includes(ev.key) &&\n                        ev.target.parentElement.querySelector(\n                            \".o-autocomplete--dropdown-item .ui-state-active\"\n                        ),\n                });\n            }\n\n            // Click on an element of a dropdown should be also completed with a selection though Enter or Tab\n            // This case is for the steps that click on a dropdown-item\n            if (element.closest(\".o-autocomplete--dropdown-menu\")) {\n                consumeEvents.push({\n                    name: \"keydown\",\n                    target: element.closest(\".o-autocomplete\").querySelector(\"input\"),\n                    conditional: (ev) => [\"Tab\", \"Enter\"].includes(ev.key),\n                });\n            }\n\n            // Press enter on a button do the same as a click\n            if (element.tagName === \"BUTTON\") {\n                consumeEvents.push({\n                    name: \"keydown\",\n                    target: element,\n                    conditional: (ev) => ev.key === \"Enter\",\n                });\n\n                // Pressing enter in the input group does the same as clicking on the button\n                if (element.closest(\".input-group\")) {\n                    for (const inputEl of element.parentElement.querySelectorAll(\"input\")) {\n                        consumeEvents.push({\n                            name: \"keydown\",\n                            target: inputEl,\n                            conditional: (ev) => ev.key === \"Enter\",\n                        });\n                    }\n                }\n            }\n        }\n\n        if ([\"fill\", \"edit\"].includes(runCommand)) {\n            if (\n                utils.isSmall() &&\n                element.closest(\".o_field_widget\")?.matches(\".o_field_many2one, .o_field_many2many\")\n            ) {\n                consumeEvents.push({\n                    name: \"click\",\n                    target: element,\n                });\n            } else {\n                consumeEvents.push({\n                    name: \"input\",\n                    target: element,\n                });\n            }\n        }\n\n        // Drag & drop run command\n        if (runCommand === \"drag\") {\n            consumeEvents.push({\n                name: \"pointerdown\",\n                target: element,\n            });\n        }\n\n        if (runCommand === \"drop\") {\n            consumeEvents.push({\n                name: \"pointerup\",\n                target: document,\n                conditional: (ev) =>\n                    element.ownerDocument\n                        .elementsFromPoint(ev.clientX, ev.clientY)\n                        .includes(element),\n            });\n        }\n\n        return consumeEvents;\n    }\n\n    /**\n     * Returns the element that will be used in listening to the `consumeEvent`.\n     * @param {HTMLElement} el\n     * @param {string} consumeEvent\n     */\n    getAnchorEl(el, consumeEvent) {\n        if (consumeEvent === \"drag\") {\n            // jQuery-ui draggable triggers 'drag' events on the .ui-draggable element,\n            // but the tip is attached to the .ui-draggable-handle element which may\n            // be one of its children (or the element itself)\n            return el.closest(\".ui-draggable, .o_draggable, .o_we_draggable, .o-draggable\");\n        }\n\n        if (consumeEvent === \"input\" && ![\"textarea\", \"input\"].includes(el.tagName.toLowerCase())) {\n            return el.closest(\"[contenteditable='true']\");\n        }\n        if (consumeEvent === \"sort\") {\n            // when an element is dragged inside a sortable container (with classname\n            // 'ui-sortable'), jQuery triggers the 'sort' event on the container\n            return el.closest(\".ui-sortable, .o_sortable\");\n        }\n        return el;\n    }\n\n    _onMutation() {\n        if (this.currentAction) {\n            const tempAnchors = this.findTriggers();\n            if (\n                tempAnchors.length &&\n                (tempAnchors.some((a) => !this.anchorEls.includes(a)) ||\n                    this.anchorEls.some((a) => !tempAnchors.includes(a)))\n            ) {\n                this.removeListeners();\n                this.anchorEls = tempAnchors;\n                this.setActionListeners();\n            } else if (!tempAnchors.length && this.anchorEls.length) {\n                this.pointer.hide();\n                if (!hoot.queryFirst(\".o_home_menu\", { visible: true })) {\n                    this.backward();\n                }\n                return;\n            }\n            this.updatePointer();\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { reactive } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { TourPointer } from \"@web_tour/tour_pointer/tour_pointer\";\nimport { getScrollParent } from \"./tour_utils\";\n\n/**\n * @typedef {import(\"@web/core/position/position_hook\").Direction} Direction\n *\n * @typedef {\"in\" | \"out-below\" | \"out-above\" | \"unknown\"} IntersectionPosition\n *\n * @typedef {ReturnType<createPointerState>[\"methods\"]} TourPointerMethods\n *\n * @typedef TourPointerState\n * @property {HTMLElement} [anchor]\n * @property {string} [content]\n * @property {boolean} [isOpen]\n * @property {() => {}} [onClick]\n * @property {() => {}} [onMouseEnter]\n * @property {() => {}} [onMouseLeave]\n * @property {boolean} isVisible\n * @property {boolean} isZone\n * @property {Direction} position\n * @property {number} rev\n *\n * @typedef {import(\"./tour_service\").TourStep} TourStep\n */\n\nclass Intersection {\n    constructor() {\n        /** @type {Element | null} */\n        this.currentTarget = null;\n        this.rootBounds = null;\n        /** @type {IntersectionPosition} */\n        this._targetPosition = \"unknown\";\n        this._observer = new IntersectionObserver((observations) =>\n            this._handleObservations(observations)\n        );\n    }\n\n    /** @type {IntersectionObserverCallback} */\n    _handleObservations(observations) {\n        if (observations.length < 1) {\n            return;\n        }\n        const observation = observations[observations.length - 1];\n        this.rootBounds = observation.rootBounds;\n        if (this.rootBounds && this.currentTarget) {\n            if (observation.isIntersecting) {\n                this._targetPosition = \"in\";\n            } else {\n                const targetBounds = this.currentTarget.getBoundingClientRect();\n                if (targetBounds.bottom < this.rootBounds.height / 2) {\n                    this._targetPosition = \"out-above\";\n                } else if (targetBounds.top > this.rootBounds.height / 2) {\n                    this._targetPosition = \"out-below\";\n                }\n            }\n        } else {\n            this._targetPosition = \"unknown\";\n        }\n    }\n\n    get targetPosition() {\n        if (!this.rootBounds) {\n            return this.currentTarget ? \"in\" : \"unknown\";\n        } else {\n            return this._targetPosition;\n        }\n    }\n\n    /**\n     * @param {Element} newTarget\n     */\n    setTarget(newTarget) {\n        if (this.currentTarget !== newTarget) {\n            if (this.currentTarget) {\n                this._observer.unobserve(this.currentTarget);\n            }\n            if (newTarget) {\n                this._observer.observe(newTarget);\n            }\n            this.currentTarget = newTarget;\n        }\n    }\n\n    stop() {\n        this._observer.disconnect();\n    }\n}\n\nexport function createPointerState() {\n    /**\n     * @param {Partial<TourPointerState>} newState\n     */\n    const setState = (newState) => {\n        Object.assign(state, newState);\n    };\n\n    /**\n     * @param {TourStep} step\n     * @param {HTMLElement} [anchor]\n     * @param {boolean} [isZone] will border de zone. e.g.: a dropzone\n     */\n    const pointTo = (anchor, step, isZone) => {\n        intersection.setTarget(anchor);\n        if (anchor) {\n            let { tooltipPosition, content } = step;\n            switch (intersection.targetPosition) {\n                case \"unknown\": {\n                    // Do nothing for unknown target position.\n                    break;\n                }\n                case \"in\": {\n                    if (document.body.contains(floatingAnchor)) {\n                        floatingAnchor.remove();\n                    }\n                    setState({\n                        anchor,\n                        content,\n                        isZone,\n                        onClick: null,\n                        position: tooltipPosition,\n                        isVisible: true,\n                    });\n                    break;\n                }\n                default: {\n                    const onClick = () => {\n                        anchor.scrollIntoView({ behavior: \"smooth\", block: \"nearest\" });\n                        hide();\n                    };\n\n                    const scrollParent = getScrollParent(anchor);\n                    if (!scrollParent) {\n                        setState({\n                            anchor,\n                            content,\n                            isZone,\n                            onClick: null,\n                            position: tooltipPosition,\n                            isVisible: true,\n                        });\n                        return;\n                    }\n                    let { x, y, width, height } = scrollParent.getBoundingClientRect();\n\n                    // If the scrolling element is within an iframe the offsets\n                    // must be computed taking into account the iframe.\n                    const iframeEl = scrollParent.ownerDocument.defaultView.frameElement;\n                    if (iframeEl) {\n                        const iframeOffset = iframeEl.getBoundingClientRect();\n                        x += iframeOffset.x;\n                        y += iframeOffset.y;\n                    }\n                    floatingAnchor.style.left = `${x + width / 2}px`;\n                    if (intersection.targetPosition === \"out-below\") {\n                        tooltipPosition = \"top\";\n                        content = _t(\"Scroll down to reach the next step.\");\n                        floatingAnchor.style.top = `${y + height - TourPointer.height}px`;\n                    } else if (intersection.targetPosition === \"out-above\") {\n                        tooltipPosition = \"bottom\";\n                        content = _t(\"Scroll up to reach the next step.\");\n                        floatingAnchor.style.top = `${y + TourPointer.height}px`;\n                    }\n                    if (!document.contains(floatingAnchor)) {\n                        document.body.appendChild(floatingAnchor);\n                    }\n                    setState({\n                        anchor: floatingAnchor,\n                        content,\n                        onClick,\n                        position: tooltipPosition,\n                        isZone,\n                        isVisible: true,\n                    });\n                }\n            }\n        } else {\n            hide();\n        }\n    };\n\n    function hide() {\n        setState({ content: \"\", isVisible: false, isOpen: false });\n    }\n\n    function showContent(isOpen) {\n        setState({ isOpen });\n    }\n\n    function destroy() {\n        intersection.stop();\n        if (document.body.contains(floatingAnchor)) {\n            floatingAnchor.remove();\n        }\n    }\n\n    /** @type {TourPointerState} */\n    const state = reactive({});\n    const intersection = new Intersection();\n    const floatingAnchor = document.createElement(\"div\");\n    floatingAnchor.className = \"position-fixed\";\n\n    return { state, setState, showContent, pointTo, hide, destroy };\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { queryAll, queryFirst, queryOne } from \"@odoo/hoot-dom\";\nimport { Component, useState, useExternalListener } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\n\nexport const TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY = \"tour_recorder_active\";\nconst PRECISE_IDENTIFIERS = [\"data-menu-xmlid\", \"name\", \"contenteditable\"];\nconst ODOO_CLASS_REGEX = /^oe?(-|_)[\\w-]+$/;\nconst VALIDATING_KEYS = [\"Enter\", \"Tab\"];\n\n/**\n * @param {EventTarget[]} paths composedPath of an click event\n * @returns {string}\n */\nconst getShortestSelector = (paths) => {\n    paths.reverse();\n    let filteredPath = [];\n    let hasOdooClass = false;\n    for (\n        let currentElem = paths.pop();\n        (currentElem && queryAll(filteredPath.join(\" > \")).length !== 1) || !hasOdooClass;\n        currentElem = paths.pop()\n    ) {\n        if (currentElem.parentElement.contentEditable === \"true\") {\n            continue;\n        }\n\n        let currentPredicate = currentElem.tagName.toLowerCase();\n        const odooClass = [...currentElem.classList].find((c) => c.match(ODOO_CLASS_REGEX));\n        if (odooClass) {\n            currentPredicate = `.${odooClass}`;\n            hasOdooClass = true;\n        }\n\n        // If we are inside a link or button the previous elements, like <i></i>, <span></span>, etc., can be removed\n        if ([\"BUTTON\", \"A\"].includes(currentElem.tagName)) {\n            filteredPath = [];\n        }\n\n        for (const identifier of PRECISE_IDENTIFIERS) {\n            const identifierValue = currentElem.getAttribute(identifier);\n            if (identifierValue) {\n                currentPredicate += `[${identifier}='${CSS.escape(identifierValue)}']`;\n            }\n        }\n\n        const siblingNodes = queryAll(\":scope > \" + currentPredicate, {\n            root: currentElem.parentElement,\n        });\n        if (siblingNodes.length > 1) {\n            currentPredicate += `:nth-child(${\n                [...currentElem.parentElement.children].indexOf(currentElem) + 1\n            })`;\n        }\n\n        filteredPath.unshift(currentPredicate);\n    }\n\n    if (filteredPath.length > 2) {\n        return reducePath(filteredPath);\n    }\n\n    return filteredPath.join(\" > \");\n};\n\n/**\n * @param {string[]} paths\n * @returns {string}\n */\nconst reducePath = (paths) => {\n    const numberOfElement = paths.length - 2;\n    let currentElement = \"\";\n    let hasReduced = false;\n    let path = paths.shift();\n    for (let i = 0; i < numberOfElement; i++) {\n        currentElement = paths.shift();\n        if (queryAll(`${path} ${paths.join(\" > \")}`).length === 1) {\n            hasReduced = true;\n        } else {\n            path += `${hasReduced ? \" \" : \" > \"}${currentElement}`;\n            hasReduced = false;\n        }\n    }\n    path += `${hasReduced ? \" \" : \" > \"}${paths.shift()}`;\n    return path;\n};\n\nexport class TourRecorder extends Component {\n    static template = \"web_tour.TourRecorder\";\n    static components = { Dropdown, DropdownItem };\n    static props = {\n        onClose: { type: Function },\n    };\n    static defaultState = {\n        recording: false,\n        url: \"\",\n        editedElement: undefined,\n        tourName: \"\",\n    };\n\n    setup() {\n        this.originClickEvent = false;\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            ...TourRecorder.defaultState,\n            steps: [],\n        });\n\n        useExternalListener(document, \"pointerdown\", this.setStartingEvent, { capture: true });\n        useExternalListener(document, \"pointerup\", this.recordClickEvent, { capture: true });\n        useExternalListener(document, \"keydown\", this.recordConfirmationKeyboardEvent, {\n            capture: true,\n        });\n        useExternalListener(document, \"keyup\", this.recordKeyboardEvent, { capture: true });\n    }\n\n    /**\n     * @param {PointerEvent} ev\n     */\n    setStartingEvent(ev) {\n        if (!this.state.recording || ev.target.closest(\".o_tour_recorder\")) {\n            return;\n        }\n        this.originClickEvent = ev.composedPath().filter((p) => p instanceof Element);\n    }\n\n    /**\n     * @param {PointerEvent} ev\n     */\n    recordClickEvent(ev) {\n        if (!this.state.recording || ev.target.closest(\".o_tour_recorder\")) {\n            return;\n        }\n        const pathElements = ev.composedPath().filter((p) => p instanceof Element);\n        this.addTourStep([...pathElements]);\n\n        const lastStepInput = this.state.steps.at(-1);\n        // Check that pointerdown and pointerup paths are different to know if it's a drag&drop or a click\n        if (\n            JSON.stringify(pathElements.map((e) => e.tagName)) !==\n            JSON.stringify(this.originClickEvent.map((e) => e.tagName))\n        ) {\n            lastStepInput.run = `drag_and_drop ${lastStepInput.trigger}`;\n            lastStepInput.trigger = getShortestSelector(this.originClickEvent);\n        } else {\n            const lastStepInput = this.state.steps.at(-1);\n            lastStepInput.run = \"click\";\n        }\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    recordConfirmationKeyboardEvent(ev) {\n        if (\n            !this.state.recording ||\n            !this.state.editedElement ||\n            ev.target.closest(\".o_tour_recorder\")\n        ) {\n            return;\n        }\n\n        if (\n            [...this.state.editedElement.classList].includes(\"o-autocomplete--input\") &&\n            VALIDATING_KEYS.includes(ev.key)\n        ) {\n            const selectedRow = queryFirst(\".ui-state-active\", {\n                root: this.state.editedElement.parentElement,\n            });\n            this.state.steps.push({\n                trigger: `.o-autocomplete--dropdown-item > a:contains('${selectedRow.textContent}'), .fa-circle-o-notch`,\n                run: \"click\",\n            });\n            this.state.editedElement = undefined;\n        }\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    recordKeyboardEvent(ev) {\n        if (\n            !this.state.recording ||\n            VALIDATING_KEYS.includes(ev.key) ||\n            ev.target.closest(\".o_tour_recorder\")\n        ) {\n            return;\n        }\n\n        if (!this.state.editedElement) {\n            if (\n                ev.target.matches(\n                    \"input:not(:disabled), textarea:not(:disabled), [contenteditable=true]\"\n                )\n            ) {\n                this.state.editedElement = ev.target;\n                this.state.steps.push({\n                    trigger: getShortestSelector(ev.composedPath()),\n                });\n            } else {\n                return;\n            }\n        }\n\n        if (!this.state.editedElement) {\n            return;\n        }\n\n        const lastStep = this.state.steps.at(-1);\n        if (this.state.editedElement.contentEditable === \"true\") {\n            lastStep.run = `editor ${this.state.editedElement.textContent}`;\n        } else {\n            lastStep.run = `edit ${this.state.editedElement.value}`;\n        }\n    }\n\n    toggleRecording() {\n        this.state.recording = !this.state.recording;\n        this.state.editedElement = undefined;\n        if (this.state.recording && !this.state.url) {\n            this.state.url = browser.location.pathname + browser.location.search;\n        }\n    }\n\n    async saveTour() {\n        const newTour = {\n            name: this.state.tourName.replaceAll(\" \", \"_\"),\n            url: this.state.url,\n            step_ids: this.state.steps.map((s) => x2ManyCommands.create(undefined, s)),\n            custom: true,\n        };\n\n        const result = await this.orm.create(\"web_tour.tour\", [newTour]);\n        if (result) {\n            this.notification.add(_t(\"Custom tour '%s' has been added.\", newTour.name), {\n                type: \"success\",\n            });\n            this.resetTourRecorderState();\n        } else {\n            this.notification.add(_t(\"Custom tour '%s' couldn't be saved!\", newTour.name), {\n                type: \"danger\",\n            });\n        }\n    }\n\n    resetTourRecorderState() {\n        Object.assign(this.state, { ...TourRecorder.defaultState, steps: [] });\n    }\n\n    /**\n     * @param {Element[]} path\n     */\n    addTourStep(path) {\n        const shortestPath = getShortestSelector(path);\n        const target = queryOne(shortestPath);\n        this.state.editedElement =\n            target.matches(\n                \"input:not(:disabled), textarea:not(:disabled), [contenteditable=true]\"\n            ) && target;\n        this.state.steps.push({\n            trigger: shortestPath,\n        });\n    }\n}\n", "/** @odoo-module **/\n\nimport { markup, whenReady, validate } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { TourPointer } from \"../tour_pointer/tour_pointer\";\nimport { createPointerState } from \"./tour_pointer_state\";\nimport { tourState } from \"./tour_state\";\nimport { TourInteractive } from \"./tour_interactive\";\nimport { TourAutomatic } from \"./tour_automatic\";\nimport { callWithUnloadCheck } from \"./tour_utils\";\nimport {\n    TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY,\n    TourRecorder,\n} from \"@web_tour/tour_service/tour_recorder/tour_recorder\";\nimport { redirect } from \"@web/core/utils/urls\";\n\nconst StepSchema = {\n    id: { type: [String], optional: true },\n    content: { type: [String, Object], optional: true }, //allow object(_t && markup)\n    debugHelp: { type: String, optional: true },\n    isActive: { type: Array, element: String, optional: true },\n    run: { type: [String, Function, Boolean], optional: true },\n    timeout: {\n        optional: true,\n        validate(value) {\n            return value >= 0 && value <= 60000;\n        },\n    },\n    tooltipPosition: {\n        optional: true,\n        validate(value) {\n            return [\"top\", \"bottom\", \"left\", \"right\"].includes(value);\n        },\n    },\n    trigger: { type: String },\n    //ONLY IN DEBUG MODE\n    pause: { type: Boolean, optional: true },\n    break: { type: Boolean, optional: true },\n};\n\nconst TourSchema = {\n    checkDelay: { type: Number, optional: true },\n    name: { type: String, optional: true },\n    saveAs: { type: String, optional: true },\n    steps: Function,\n    url: { type: String, optional: true },\n    wait_for: { type: [Function, Object], optional: true },\n};\n\nregistry.category(\"web_tour.tours\").addValidation(TourSchema);\nconst userMenuRegistry = registry.category(\"user_menuitems\");\n\nexport const tourService = {\n    // localization dependency to make sure translations used by tours are loaded\n    dependencies: [\"orm\", \"effect\", \"overlay\", \"localization\"],\n    start: async (_env, { orm, effect, overlay }) => {\n        await whenReady();\n        let toursEnabled = session?.tour_enabled;\n        const tourRegistry = registry.category(\"web_tour.tours\");\n        const pointer = createPointerState();\n        pointer.stop = () => {};\n\n        userMenuRegistry.add(\"web_tour.tour_enabled\", () => ({\n            type: \"switch\",\n            id: \"web_tour.tour_enabled\",\n            description: _t(\"Onboarding\"),\n            callback: async () => {\n                tourState.clear();\n                toursEnabled = await orm.call(\"res.users\", \"switch_tour_enabled\", [!toursEnabled]);\n                browser.location.reload();\n            },\n            isChecked: toursEnabled,\n            sequence: 30,\n        }));\n\n        function endTour({ name }) {\n            // Used to signal the python test runner that the tour finished without error.\n            browser.console.log(\"tour succeeded\");\n            // Used to see easily in the python console and to know which tour has been succeeded in suite tours case.\n            const succeeded = `\u2551 TOUR ${name} SUCCEEDED \u2551`;\n            const msg = [succeeded];\n            msg.unshift(\"\u2554\" + \"\u2550\".repeat(succeeded.length - 2) + \"\u2557\");\n            msg.push(\"\u255a\" + \"\u2550\".repeat(succeeded.length - 2) + \"\u255d\");\n            browser.console.log(`\\n\\n${msg.join(\"\\n\")}\\n`);\n            tourState.clear();\n        }\n\n        function getTourFromRegistry(tourName) {\n            const tour = tourRegistry.getEntries().findLast(([n, t]) => t.saveAs == tourName) || [\n                tourName,\n                tourRegistry.get(tourName),\n            ];\n\n            return {\n                ...tour[1],\n                steps: tour[1].steps(),\n                name: tour[0],\n                wait_for: tour[1].wait_for || Promise.resolve(),\n            };\n        }\n\n        async function getTourFromDB(tourName) {\n            const tour = await orm.call(\"web_tour.tour\", \"get_tour_json_by_name\", [tourName]);\n            if (!tour) {\n                throw new Error(`Tour '${tourName}' is not found in the database.`);\n            }\n\n            if (!tour.steps.length && tourRegistry.contains(tour.name)) {\n                tour.steps = tourRegistry.get(tour.name).steps();\n            }\n\n            return tour;\n        }\n\n        function validateStep(step) {\n            try {\n                validate(step, StepSchema);\n            } catch (error) {\n                console.error(\n                    `Error in schema for TourStep ${JSON.stringify(step, null, 4)}\\n${\n                        error.message\n                    }`\n                );\n            }\n        }\n\n        async function startTour(tourName, options = {}) {\n            pointer.stop();\n            const tour = options.fromDB\n                ? { name: tourName, url: options.url }\n                : getTourFromRegistry(tourName);\n\n            if (!session.is_public && !toursEnabled && options.mode === \"manual\") {\n                toursEnabled = await orm.call(\"res.users\", \"switch_tour_enabled\", [!toursEnabled]);\n            }\n\n            let tourConfig = {\n                stepDelay: 0,\n                keepWatchBrowser: false,\n                mode: \"auto\",\n                showPointerDuration: 0,\n                debug: false,\n                redirect: true,\n            };\n\n            tourConfig = Object.assign(tourConfig, options);\n            tourState.setCurrentConfig(tourConfig);\n            tourState.setCurrentTour(tour.name);\n            tourState.setCurrentIndex(0);\n            if (tourConfig.debug !== false) {\n                // Starts the tour with a debugger to allow you to choose devtools configuration.\n                // eslint-disable-next-line no-debugger\n                debugger;\n            }\n\n            const willUnload = callWithUnloadCheck(() => {\n                if (tour.url && tourConfig.startUrl != tour.url && tourConfig.redirect) {\n                    redirect(tour.url);\n                }\n            });\n            if (!willUnload) {\n                resumeTour();\n            }\n        }\n\n        async function resumeTour() {\n            const tourName = tourState.getCurrentTour();\n            const tourConfig = tourState.getCurrentConfig();\n\n            let tour;\n            if (tourConfig.fromDB) {\n                tour = await getTourFromDB(tourName);\n            } else if (tourRegistry.contains(tourName)) {\n                tour = getTourFromRegistry(tourName);\n            }\n\n            if (!tour) {\n                return;\n            }\n\n            tour.steps.forEach((step) => validateStep(step));\n            pointer.stop = overlay.add(\n                TourPointer,\n                {\n                    pointerState: pointer.state,\n                    bounce: !(tourConfig.mode === \"auto\" && tourConfig.keepWatchBrowser),\n                },\n                {\n                    sequence: 1100, // sequence based on bootstrap z-index values.\n                }\n            );\n\n            if (tourConfig.mode === \"auto\") {\n                new TourAutomatic(tour).start(pointer, () => {\n                    pointer.stop();\n                    endTour(tour);\n                });\n            } else {\n                new TourInteractive(tour).start(pointer, async () => {\n                    pointer.stop();\n                    endTour(tour);\n                    let message = tourConfig.rainbowManMessage || tour.rainbowManMessage;\n                    if (message) {\n                        message = window.DOMPurify.sanitize(tourConfig.rainbowManMessage);\n                        effect.add({\n                            type: \"rainbow_man\",\n                            message: markup(message),\n                        });\n                    }\n\n                    const nextTour = await orm.call(\"web_tour.tour\", \"consume\", [tour.name]);\n                    if (nextTour) {\n                        startTour(nextTour.name, {\n                            mode: \"manual\",\n                            redirect: false,\n                            rainbowManMessage: nextTour.rainbowManMessage,\n                        });\n                    }\n                });\n            }\n        }\n\n        function startTourRecorder() {\n            if (!browser.localStorage.getItem(TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY)) {\n                const remove = overlay.add(\n                    TourRecorder,\n                    {\n                        onClose: () => {\n                            remove();\n                            browser.localStorage.removeItem(TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY);\n                        },\n                    },\n                    { sequence: 99999 }\n                );\n            }\n            browser.localStorage.setItem(TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY, \"1\");\n        }\n\n        if (!window.frameElement) {\n            const paramsTourName = new URLSearchParams(browser.location.search).get(\"tour\");\n            if (paramsTourName) {\n                startTour(paramsTourName, { mode: \"manual\", fromDB: true });\n            }\n\n            if (tourState.getCurrentTour()) {\n                if (tourState.getCurrentConfig().mode === \"auto\" || toursEnabled) {\n                    resumeTour();\n                } else {\n                    tourState.clear();\n                }\n            } else if (session.current_tour) {\n                startTour(session.current_tour.name, {\n                    mode: \"manual\",\n                    redirect: false,\n                    rainbowManMessage: session.current_tour.rainbowManMessage,\n                });\n            }\n\n            if (\n                browser.localStorage.getItem(TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY) &&\n                !session.is_public\n            ) {\n                const remove = overlay.add(\n                    TourRecorder,\n                    {\n                        onClose: () => {\n                            remove();\n                            browser.localStorage.removeItem(TOUR_RECORDER_ACTIVE_LOCAL_STORAGE_KEY);\n                        },\n                    },\n                    { sequence: 99999 }\n                );\n            }\n        }\n\n        odoo.startTour = startTour;\n        odoo.isTourReady = (tourName) => getTourFromRegistry(tourName).wait_for.then(() => true);\n\n        return {\n            startTour,\n            startTourRecorder,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"tour_service\", tourService);\n", "/** @odoo-module **/\n\nimport { browser } from \"@web/core/browser/browser\";\n\nconst CURRENT_TOUR_LOCAL_STORAGE = \"current_tour\";\nconst CURRENT_TOUR_CONFIG_LOCAL_STORAGE = \"current_tour.config\";\nconst CURRENT_TOUR_INDEX_LOCAL_STORAGE = \"current_tour.index\";\nconst CURRENT_TOUR_ON_ERROR_LOCAL_STORAGE = \"current_tour.on_error\";\n\n/**\n * Wrapper around localStorage for persistence of the running tours.\n * Useful for resuming running tours when the page refreshed.\n */\nexport const tourState = {\n    getCurrentTour() {\n        return browser.localStorage.getItem(CURRENT_TOUR_LOCAL_STORAGE);\n    },\n    setCurrentTour(tourName) {\n        browser.localStorage.setItem(CURRENT_TOUR_LOCAL_STORAGE, tourName);\n    },\n    getCurrentIndex() {\n        const index = browser.localStorage.getItem(CURRENT_TOUR_INDEX_LOCAL_STORAGE, \"0\");\n        return parseInt(index, 10);\n    },\n    setCurrentIndex(index) {\n        browser.localStorage.setItem(CURRENT_TOUR_INDEX_LOCAL_STORAGE, index.toString());\n    },\n    getCurrentConfig() {\n        const config = browser.localStorage.getItem(CURRENT_TOUR_CONFIG_LOCAL_STORAGE, \"{}\");\n        return JSON.parse(config);\n    },\n    setCurrentConfig(config) {\n        config = JSON.stringify(config);\n        browser.localStorage.setItem(CURRENT_TOUR_CONFIG_LOCAL_STORAGE, config);\n    },\n    getCurrentTourOnError() {\n        return browser.localStorage.getItem(CURRENT_TOUR_ON_ERROR_LOCAL_STORAGE);\n    },\n    setCurrentTourOnError() {\n        browser.localStorage.setItem(CURRENT_TOUR_ON_ERROR_LOCAL_STORAGE, \"1\");\n    },\n    clear() {\n        browser.localStorage.removeItem(CURRENT_TOUR_ON_ERROR_LOCAL_STORAGE);\n        browser.localStorage.removeItem(CURRENT_TOUR_CONFIG_LOCAL_STORAGE);\n        browser.localStorage.removeItem(CURRENT_TOUR_INDEX_LOCAL_STORAGE);\n        browser.localStorage.removeItem(CURRENT_TOUR_LOCAL_STORAGE);\n    },\n};\n", "import { session } from \"@web/session\";\nimport { utils } from \"@web/core/ui/ui_service\";\nimport * as hoot from \"@odoo/hoot-dom\";\nimport { pick } from \"@web/core/utils/objects\";\n\n/**\n * @typedef TourStep\n * @property {\"enterprise\"|\"community\"|\"mobile\"|\"desktop\"|HootSelector[][]} isActive Active the step following {@link isActiveStep} filter\n * @property {string} [id]\n * @property {HootSelector} trigger The node on which the action will be executed.\n * @property {string} [content] Description of the step.\n * @property {\"top\" | \"botton\" | \"left\" | \"right\"} [position] The position where the UI helper is shown.\n * @property {RunCommand} [run] The action to perform when trigger conditions are verified.\n * @property {number} [timeout] By default, when the trigger node isn't found after 10000 milliseconds, it throws an error.\n * You can change this value to lengthen or shorten the time before the error occurs [ms].\n */\nexport class TourStep {\n    constructor(data, tour) {\n        Object.assign(this, data);\n        this.tour = tour;\n    }\n\n    /**\n     * Check if a step is active dependant on step.isActive property\n     * Note that when step.isActive is not defined, the step is active by default.\n     * When a step is not active, it's just skipped and the tour continues to the next step.\n     */\n    get active() {\n        this.checkHasTour();\n        const mode = this.tour.mode;\n        const isSmall = utils.isSmall();\n        const standardKeyWords = [\"enterprise\", \"community\", \"mobile\", \"desktop\", \"auto\", \"manual\"];\n        const isActiveArray = Array.isArray(this.isActive) ? this.isActive : [];\n        if (isActiveArray.length === 0) {\n            return true;\n        }\n        const selectors = isActiveArray.filter((key) => !standardKeyWords.includes(key));\n        if (selectors.length) {\n            // if one of selectors is not found, step is skipped\n            for (const selector of selectors) {\n                const el = hoot.queryFirst(selector);\n                if (!el) {\n                    return false;\n                }\n            }\n        }\n        const checkMode =\n            isActiveArray.includes(mode) ||\n            (!isActiveArray.includes(\"manual\") && !isActiveArray.includes(\"auto\"));\n        const edition =\n            (session.server_version_info || \"\").at(-1) === \"e\" ? \"enterprise\" : \"community\";\n        const checkEdition =\n            isActiveArray.includes(edition) ||\n            (!isActiveArray.includes(\"enterprise\") && !isActiveArray.includes(\"community\"));\n        const onlyForMobile = isActiveArray.includes(\"mobile\") && isSmall;\n        const onlyForDesktop = isActiveArray.includes(\"desktop\") && !isSmall;\n        const checkDevice =\n            onlyForMobile ||\n            onlyForDesktop ||\n            (!isActiveArray.includes(\"mobile\") && !isActiveArray.includes(\"desktop\"));\n        return checkEdition && checkDevice && checkMode;\n    }\n\n    checkHasTour() {\n        if (!this.tour) {\n            throw new Error(`TourStep instance must have a tour`);\n        }\n    }\n\n    get describeMe() {\n        this.checkHasTour();\n        return (\n            `[${this.index + 1}/${this.tour.steps.length}] Tour ${this.tour.name} \u2192 Step ` +\n            (this.content ? `${this.content} (trigger: ${this.trigger})` : this.trigger)\n        );\n    }\n\n    get stringify() {\n        return (\n            JSON.stringify(\n                pick(this, \"isActive\", \"content\", \"trigger\", \"run\", \"tooltipPosition\", \"timeout\"),\n                (_key, value) => {\n                    if (typeof value === \"function\") {\n                        return \"[function]\";\n                    } else {\n                        return value;\n                    }\n                },\n                2\n            ) + \",\"\n        );\n    }\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { _legacyIsVisible } from \"@web/core/utils/ui\";\nimport { tourState } from \"./tour_state\";\nimport * as hoot from \"@odoo/hoot-dom\";\nimport { setupEventActions } from \"@web/../lib/hoot-dom/helpers/events\";\nimport { callWithUnloadCheck } from \"./tour_utils\";\nimport { TourHelpers } from \"./tour_helpers\";\nimport { TourStep } from \"./tour_step\";\n\nexport class TourStepAutomatic extends TourStep {\n    triggerFound = false;\n    hasRun = false;\n    isBlocked = false;\n    running = false;\n    constructor(data, tour, index) {\n        super(data, tour);\n        this.index = index;\n        this.tourConfig = tourState.getCurrentConfig();\n    }\n\n    get canContinue() {\n        this.isBlocked =\n            document.body.classList.contains(\"o_ui_blocked\") ||\n            document.querySelector(\".o_blockUI\");\n        return !this.isBlocked;\n    }\n\n    /**\n     * @type {TourStepCompiler}\n     * @param {TourStep} step\n     * @param {object} options\n     * @returns {{trigger, action}[]}\n     */\n    compileToMacro(pointer) {\n        const debugMode = this.tourConfig.debug;\n\n        return [\n            {\n                action: () => {\n                    this.running = true;\n                    setupEventActions(document.createElement(\"div\"));\n                    if (this.break && debugMode !== false) {\n                        // eslint-disable-next-line no-debugger\n                        debugger;\n                    }\n                },\n            },\n            {\n                action: async () => {\n                    if (debugMode === false) {\n                        console.log(this.describeMe);\n                    } else {\n                        console.groupCollapsed(this.describeMe);\n                        console.log(this.stringify);\n                        console.groupEnd();\n                    }\n                    this._timeout = browser.setTimeout(\n                        () => this.throwError(),\n                        (this.timeout || 10000) + this.tour.stepDelay\n                    );\n                    // This delay is important for making the current set of tour tests pass.\n                    // IMPROVEMENT: Find a way to remove this delay.\n                    await new Promise((resolve) => requestAnimationFrame(resolve));\n                    await new Promise((resolve) =>\n                        browser.setTimeout(resolve, this.tour.stepDelay)\n                    );\n                },\n            },\n            {\n                trigger: () => {\n                    if (!this.active) {\n                        this.run = () => {};\n                        return true;\n                    }\n                    const stepEl = this.findTrigger();\n                    if (!stepEl) {\n                        return false;\n                    }\n                    return this.canContinue && stepEl;\n                },\n                action: async (stepEl) => {\n                    clearTimeout(this._timeout);\n                    tourState.setCurrentIndex(this.index + 1);\n                    if (this.tour.showPointerDuration > 0 && stepEl !== true) {\n                        // Useful in watch mode.\n                        pointer.pointTo(stepEl, this);\n                        await new Promise((r) =>\n                            browser.setTimeout(r, this.tour.showPointerDuration)\n                        );\n                        pointer.hide();\n                    }\n\n                    // TODO: Delegate the following routine to the `ACTION_HELPERS` in the macro module.\n                    const actionHelper = new TourHelpers(stepEl);\n\n                    let result;\n                    if (typeof this.run === \"function\") {\n                        const willUnload = await callWithUnloadCheck(async () => {\n                            await this.tryToDoAction(() =>\n                                // `this.anchor` is expected in many `step.run`.\n                                this.run.call({ anchor: stepEl }, actionHelper)\n                            );\n                        });\n                        result = willUnload && \"will unload\";\n                    } else if (typeof this.run === \"string\") {\n                        for (const todo of this.run.split(\"&&\")) {\n                            const m = String(todo)\n                                .trim()\n                                .match(/^(?<action>\\w*) *\\(? *(?<arguments>.*?)\\)?$/);\n                            await this.tryToDoAction(() =>\n                                actionHelper[m.groups?.action](m.groups?.arguments)\n                            );\n                        }\n                    }\n                    return result;\n                },\n            },\n            {\n                action: async () => {\n                    if (this.pause && debugMode !== false) {\n                        const styles = [\n                            \"background: black; color: white; font-size: 14px\",\n                            \"background: black; color: orange; font-size: 14px\",\n                        ];\n                        console.log(\n                            `%cTour is paused. Use %cplay()%c to continue.`,\n                            styles[0],\n                            styles[1],\n                            styles[0]\n                        );\n                        await new Promise((resolve) => {\n                            window.play = () => {\n                                resolve();\n                                delete window.play;\n                            };\n                        });\n                    }\n                    this.running = false;\n                },\n            },\n        ];\n    }\n\n    get describeWhyIFailed() {\n        if (!this.triggerFound) {\n            return `The cause is that trigger (${this.trigger}) element cannot be found in DOM. TIP: You can use :not(:visible) to force the search for an invisible element.`;\n        } else if (this.isBlocked) {\n            return \"Element has been found but DOM is blocked by UI.\";\n        } else if (this.hasModal) {\n            return `Element has been found but it's not allowed to do action on an element that's below a modal.`;\n        } else if (!this.hasRun) {\n            return `Element has been found. The error seems to be with step.run.`;\n        }\n        return \"\";\n    }\n\n    get describeWhyIFailedDetailed() {\n        const offset = 3;\n        const start = Math.max(this.index - offset, 0);\n        const end = Math.min(this.index + offset, this.tour.steps.length - 1);\n        const result = [];\n        for (let i = start; i <= end; i++) {\n            const stepString = new TourStep(this.tour.steps[i]).stringify;\n            const text = [stepString];\n            if (i === this.index) {\n                const line = \"-\".repeat(10);\n                const failing_step = `${line} FAILED: ${this.describeMe} ${line}`;\n                text.unshift(failing_step);\n                text.push(\"-\".repeat(failing_step.length));\n            }\n            result.push(...text);\n        }\n        return result.join(\"\\n\");\n    }\n\n    /**\n     * @returns {HTMLElement}\n     */\n    findTrigger() {\n        let nodes;\n        try {\n            nodes = hoot.queryAll(this.trigger);\n        } catch (error) {\n            this.throwError(`Trigger was not found : ${this.trigger} : ${error.message}`);\n        }\n        const triggerEl = this.trigger.includes(\":visible\")\n            ? nodes.at(0)\n            : nodes.find(_legacyIsVisible);\n        this.triggerFound = !!triggerEl;\n        if (triggerEl && this.hasAction) {\n            const overlays = hoot.queryFirst(\".popover, .o-we-command, .o_notification\");\n            this.hasModal = hoot.queryFirst(\".modal:visible:not(.o_inactive_modal):last\");\n            if (this.hasModal && !overlays && !this.trigger.startsWith(\"body\")) {\n                return this.hasModal.contains(hoot.getParentFrame(triggerEl)) ||\n                    this.hasModal.contains(triggerEl)\n                    ? triggerEl\n                    : false;\n            }\n        }\n        return triggerEl;\n    }\n\n    get hasAction() {\n        return [\"string\", \"function\"].includes(typeof this.run);\n    }\n\n    /**\n     * @param {string} [error]\n     */\n    throwError(error = \"\") {\n        tourState.setCurrentTourOnError();\n        const tourConfig = tourState.getCurrentConfig();\n        // console.error notifies the test runner that the tour failed.\n        const errors = [`FAILED: ${this.describeMe}.`, this.describeWhyIFailed, error];\n        console.error(errors.filter(Boolean).join(\"\\n\"));\n        // The logged text shows the relative position of the failed step.\n        // Useful for finding the failed step.\n        console.dir(this.describeWhyIFailedDetailed);\n        if (tourConfig.debug !== false) {\n            // eslint-disable-next-line no-debugger\n            debugger;\n        }\n    }\n\n    async tryToDoAction(action) {\n        try {\n            await action();\n            this.hasRun = true;\n        } catch (error) {\n            this.throwError(error.message);\n        }\n    }\n}\n", "/** @odoo-module **/\nimport * as hoot from \"@odoo/hoot-dom\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * Calls the given `func` then returns/resolves to `true`\n * if it will result to unloading of the page.\n * @param {(...args: any[]) => void} func\n * @param  {any[]} args\n * @returns {boolean | Promise<boolean>}\n */\nexport function callWithUnloadCheck(func, ...args) {\n    let willUnload = false;\n    const beforeunload = () => (willUnload = true);\n    window.addEventListener(\"beforeunload\", beforeunload);\n    const result = func(...args);\n    if (result instanceof Promise) {\n        return result.then(() => {\n            window.removeEventListener(\"beforeunload\", beforeunload);\n            return willUnload;\n        });\n    } else {\n        window.removeEventListener(\"beforeunload\", beforeunload);\n        return willUnload;\n    }\n}\n\n/**\n * @param {HTMLElement} element\n * @returns {HTMLElement | null}\n */\nexport function getScrollParent(element) {\n    if (!element) {\n        return null;\n    }\n    // We cannot only rely on the fact that the element\u2019s scrollHeight is\n    // greater than its clientHeight. This might not be the case when a step\n    // starts, and the scrollbar could appear later. For example, when clicking\n    // on a \"building block\" in the \"building block previews modal\" during a\n    // tour (in website edit mode). When the modal opens, not all \"building\n    // blocks\" are loaded yet, and the scrollbar is not present initially.\n    const overflowY = window.getComputedStyle(element).overflowY;\n    const isScrollable =\n        overflowY === \"auto\" ||\n        overflowY === \"scroll\" ||\n        (overflowY === \"visible\" && element === element.ownerDocument.scrollingElement);\n    if (isScrollable) {\n        return element;\n    } else {\n        return getScrollParent(element.parentNode);\n    }\n}\n\nexport const stepUtils = {\n    _getHelpMessage(functionName, ...args) {\n        return `Generated by function tour utils ${functionName}(${args.join(\", \")})`;\n    },\n\n    addDebugHelp(helpMessage, step) {\n        if (typeof step.debugHelp === \"string\") {\n            step.debugHelp = step.debugHelp + \"\\n\" + helpMessage;\n        } else {\n            step.debugHelp = helpMessage;\n        }\n        return step;\n    },\n\n    showAppsMenuItem() {\n        return {\n            isActive: [\"auto\", \"community\", \"desktop\"],\n            trigger: \".o_navbar_apps_menu button:enabled\",\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        };\n    },\n\n    toggleHomeMenu() {\n        return [\n            {\n                isActive: [\".o_main_navbar .o_menu_toggle\"],\n                trigger: \".o_main_navbar .o_menu_toggle\",\n                content: _t(\"Click the top left corner to navigate across apps.\"),\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            },\n            {\n                isActive: [\"mobile\"],\n                trigger: \".o_sidebar_topbar a.btn-primary\",\n                tooltipPosition: \"right\",\n                run: \"click\",\n            },\n        ];\n    },\n\n    autoExpandMoreButtons(isActiveMobile = false) {\n        const isActive = [\"auto\"];\n        if (isActiveMobile) {\n            isActive.push(\"mobile\");\n        }\n        return {\n            isActive,\n            content: `autoExpandMoreButtons`,\n            trigger: \".o-form-buttonbox\",\n            run() {\n                const more = hoot.queryFirst(\".o-form-buttonbox .o_button_more\");\n                if (more) {\n                    hoot.click(more);\n                }\n            },\n        };\n    },\n\n    goToAppSteps(dataMenuXmlid, description) {\n        return [\n            this.showAppsMenuItem(),\n            {\n                isActive: [\"community\"],\n                trigger: `.o_app[data-menu-xmlid=\"${dataMenuXmlid}\"]`,\n                content: description,\n                tooltipPosition: \"right\",\n                run: \"click\",\n            },\n            {\n                isActive: [\"enterprise\"],\n                trigger: `.o_app[data-menu-xmlid=\"${dataMenuXmlid}\"]`,\n                content: description,\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            },\n        ].map((step) =>\n            this.addDebugHelp(this._getHelpMessage(\"goToApp\", dataMenuXmlid, description), step)\n        );\n    },\n\n    statusbarButtonsSteps(innerTextButton, description, trigger) {\n        const steps = [];\n        if (trigger) {\n            steps.push({\n                isActive: [\"auto\", \"mobile\"],\n                trigger,\n            });\n        }\n        steps.push(\n            {\n                isActive: [\"auto\", \"mobile\"],\n                trigger: \".o_cp_action_menus\",\n                run: (actions) => {\n                    const node = hoot.queryFirst(\".o_cp_action_menus .fa-cog\");\n                    if (node) {\n                        hoot.click(node);\n                    }\n                },\n            },\n            {\n                trigger: `.o_statusbar_buttons button:enabled:contains('${innerTextButton}'), .dropdown-item button:enabled:contains('${innerTextButton}')`,\n                content: description,\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            }\n        );\n        return steps.map((step) =>\n            this.addDebugHelp(\n                this._getHelpMessage(\"statusbarButtonsSteps\", innerTextButton, description),\n                step\n            )\n        );\n    },\n\n    mobileKanbanSearchMany2X(modalTitle, valueSearched) {\n        return [\n            {\n                isActive: [\"mobile\"],\n                trigger: `.modal:not(.o_inactive_modal) .o_control_panel_navigation .btn .fa-search`,\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            },\n            {\n                isActive: [\"mobile\"],\n                trigger: \".o_searchview_input\",\n                tooltipPosition: \"bottom\",\n                run: `edit ${valueSearched}`,\n            },\n            {\n                isActive: [\"mobile\"],\n                trigger: \".dropdown-menu.o_searchview_autocomplete\",\n            },\n            {\n                isActive: [\"mobile\"],\n                trigger: \".o_searchview_input\",\n                tooltipPosition: \"bottom\",\n                run: \"press Enter\",\n            },\n            {\n                isActive: [\"mobile\"],\n                trigger: `.o_kanban_record:contains('${valueSearched}')`,\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            },\n        ].map((step) =>\n            this.addDebugHelp(\n                this._getHelpMessage(\"mobileKanbanSearchMany2X\", modalTitle, valueSearched),\n                step\n            )\n        );\n    },\n    /**\n     * Utility steps to save a form and wait for the save to complete\n     */\n    saveForm() {\n        return [\n            {\n                isActive: [\"auto\"],\n                content: \"save form\",\n                trigger: \".o_form_button_save:enabled\",\n                run: \"click\",\n            },\n            {\n                isActive: [\"auto\"],\n                content: \"wait for save completion\",\n                trigger: \".o_form_readonly, .o_form_saved\",\n            },\n        ];\n    },\n    /**\n     * Utility steps to cancel a form creation or edition.\n     *\n     * Supports creation/edition from either a form or a list view (so checks\n     * for both states).\n     */\n    discardForm() {\n        return [\n            {\n                isActive: [\"auto\"],\n                content: \"discard the form\",\n                trigger: \".o_form_button_cancel\",\n                run: \"click\",\n            },\n            {\n                isActive: [\"auto\"],\n                content: \"wait for cancellation to complete\",\n                trigger:\n                    \".o_view_controller.o_list_view, .o_form_view > div > div > .o_form_readonly, .o_form_view > div > div > .o_form_saved\",\n            },\n        ];\n    },\n\n    waitIframeIsReady() {\n        return {\n            content: \"Wait until the iframe is ready\",\n            trigger: `iframe[is-ready=true]:iframe html`,\n        };\n    },\n\n    goToUrl(url) {\n        return {\n            isActive: [\"auto\"],\n            content: `Navigate to ${url}`,\n            trigger: \"body\",\n            run: `goToUrl ${url}`,\n        };\n    },\n};\n", "/** @odoo-module */\n\nimport { HootDomError, getTag, isFirefox, isIterable, parseRegExp } from \"../hoot_dom_utils\";\nimport { Deferred, waitUntil } from \"./time\";\n\n/**\n * @typedef {number | [number, number] | {\n *  w?: number;\n *  h?: number;\n *  width?: number;\n *  height?: number;\n * }} Dimensions\n *\n * @typedef {{\n *  root?: Target;\n *  tabbable?: boolean;\n * }} FocusableOptions\n *\n * @typedef {{\n *  keepInlineTextNodes?: boolean;\n *  tabSize?: number;\n *  type?: \"html\" | \"xml\";\n * }} FormatXmlOptions\n *\n * @typedef {{\n *  inline: boolean;\n *  level: number;\n *  value: MarkupLayerValue;\n * }} MarkupLayer\n *\n * @typedef {{\n *  close?: string;\n *  open?: string;\n *  textContent?: string;\n * }} MarkupLayerValue\n *\n * @typedef {(node: Node, selector: string) => Node[]} NodeGetter\n *\n * @typedef {string | string[] | number | boolean | File[]} NodeValue\n *\n * @typedef {number | [number, number] | {\n *  x?: number;\n *  y?: number;\n *  left?: number;\n *  top?: number,\n *  clientX?: number;\n *  clientY?: number;\n *  pageX?: number;\n *  pageY?: number;\n *  screenX?: number;\n *  screenY?: number;\n * }} Position\n *\n * @typedef {(content: string) => (node: Node, index: number, nodes: Node[]) => boolean | Node} PseudoClassPredicateBuilder\n *\n * @typedef {{\n *  displayed?: boolean;\n *  exact?: number;\n *  root?: HTMLElement;\n *  viewPort?: boolean;\n *  visible?: boolean;\n * }} QueryOptions\n *\n * @typedef {{\n *  trimPadding?: boolean;\n * }} QueryRectOptions\n *\n * @typedef {{\n *  raw?: boolean;\n * }} QueryTextOptions\n *\n * @typedef {import(\"./time\").WaitOptions} WaitOptions\n */\n\n/**\n * @template T\n * @typedef {T | Iterable<T>} MaybeIterable\n */\n\n/**\n * @template [T=Node]\n * @typedef {MaybeIterable<T> | string | null | undefined | false} Target\n */\n\n//-----------------------------------------------------------------------------\n// Global\n//-----------------------------------------------------------------------------\n\nconst {\n    Boolean,\n    document,\n    DOMParser,\n    innerWidth,\n    innerHeight,\n    Map,\n    MutationObserver,\n    Number: { isInteger: $isInteger, isNaN: $isNaN, parseInt: $parseInt, parseFloat: $parseFloat },\n    Object: { keys: $keys, values: $values },\n    RegExp,\n    Set,\n} = globalThis;\n\n//-----------------------------------------------------------------------------\n// Internal\n//-----------------------------------------------------------------------------\n\n/**\n * @param  {string[]} values\n */\nconst and = (values) => {\n    const last = values.pop();\n    if (values.length) {\n        return [values.join(\", \"), last].join(\" and \");\n    } else {\n        return last;\n    }\n};\n\nconst compilePseudoClassRegex = () => {\n    const customKeys = [...customPseudoClasses.keys()].filter((k) => k !== \"has\" && k !== \"not\");\n    return new RegExp(`:(${customKeys.join(\"|\")})`);\n};\n\n/**\n * @param {Element[]} elements\n * @param {string} selector\n */\nconst elementsMatch = (elements, selector) => {\n    if (!elements.length) {\n        return false;\n    }\n    return parseSelector(selector).some((selectorParts) => {\n        const [baseSelector, ...filters] = selectorParts.at(-1);\n        for (let i = 0; i < elements.length; i++) {\n            if (baseSelector && !elements[i].matches(baseSelector)) {\n                return false;\n            }\n            if (!filters.every((filter) => matchFilter(filter, elements, i))) {\n                return false;\n            }\n        }\n        return true;\n    });\n};\n\n/**\n * @param {Node} node\n * @returns {Element | null}\n */\nconst ensureElement = (node) => {\n    if (node) {\n        if (isDocument(node)) {\n            return node.documentElement;\n        }\n        if (isWindow(node)) {\n            return node.document.documentElement;\n        }\n        if (isElement(node)) {\n            return node;\n        }\n    }\n    return null;\n};\n\n/**\n * @param {Iterable<Node>} nodes\n * @param {number} level\n * @param {boolean} [keepInlineTextNodes]\n */\nconst extractLayers = (nodes, level, keepInlineTextNodes) => {\n    /** @type {MarkupLayer[]} */\n    const layers = [];\n    for (const node of nodes) {\n        if (node.nodeType === Node.COMMENT_NODE) {\n            continue;\n        }\n        if (node.nodeType === Node.TEXT_NODE) {\n            const textContent = node.nodeValue.replaceAll(/\\n/g, \"\");\n            const trimmedTextContent = textContent.trim();\n            if (trimmedTextContent) {\n                const inline = textContent === trimmedTextContent;\n                layers.push({ inline, level, value: { textContent: trimmedTextContent } });\n            }\n            continue;\n        }\n        const [open, close] = node.outerHTML.replace(`>${node.innerHTML}<`, \">\\n<\").split(\"\\n\");\n        const layer = { inline: false, level, value: { open, close } };\n        layers.push(layer);\n        const childLayers = extractLayers(node.childNodes, level + 1, false);\n        if (keepInlineTextNodes && childLayers.length === 1 && childLayers[0].inline) {\n            layer.value.textContent = childLayers[0].value.textContent;\n        } else {\n            layers.push(...childLayers);\n        }\n    }\n    return layers;\n};\n\n/**\n * @param {Iterable<Node>} nodesToFilter\n */\nconst filterUniqueNodes = (nodesToFilter) => {\n    /** @type {Node[]} */\n    const nodes = [];\n    for (const node of nodesToFilter) {\n        if (isQueryableNode(node) && !nodes.includes(node)) {\n            nodes.push(node);\n        }\n    }\n    return nodes;\n};\n\n/**\n * @param {MarkupLayer[]} layers\n * @param {number} tabSize\n */\nconst generateStringFromLayers = (layers, tabSize) => {\n    const result = [];\n    let layerIndex = 0;\n    while (layers.length > 0) {\n        const layer = layers[layerIndex];\n        const { level, value } = layer;\n        const pad = \" \".repeat(tabSize * level);\n        let nextLayerIndex = layerIndex + 1;\n        if (value.open) {\n            if (value.textContent) {\n                // node with inline textContent (no wrapping white-spaces)\n                result.push(`${pad}${value.open}${value.textContent}${value.close}`);\n                layers.splice(layerIndex, 1);\n                nextLayerIndex--;\n            } else {\n                result.push(`${pad}${value.open}`);\n                delete value.open;\n            }\n        } else {\n            if (value.close) {\n                result.push(`${pad}${value.close}`);\n            } else if (value.textContent) {\n                result.push(`${pad}${value.textContent}`);\n            }\n            layers.splice(layerIndex, 1);\n            nextLayerIndex--;\n        }\n        if (nextLayerIndex >= layers.length) {\n            layerIndex = nextLayerIndex - 1;\n            continue;\n        }\n        const nextLayer = layers[nextLayerIndex];\n        if (nextLayerIndex === 0 || nextLayer.level > layers[nextLayerIndex - 1].level) {\n            layerIndex = nextLayerIndex;\n        } else {\n            layerIndex = nextLayerIndex - 1;\n        }\n    }\n    return result.join(\"\\n\");\n};\n\n/**\n * @param {Node} node\n * @returns {NodeValue}\n */\nconst getNodeContent = (node) => {\n    switch (getTag(node)) {\n        case \"input\":\n        case \"option\":\n        case \"textarea\":\n            return getNodeValue(node);\n        case \"select\":\n            return [...node.selectedOptions].map(getNodeValue).join(\",\");\n    }\n    return getNodeText(node);\n};\n\n/**\n * @param {string} string\n */\nconst getStringContent = (string) => string.match(R_QUOTE_CONTENT)?.[2] || string;\n\n/**\n * @param {string} [char]\n */\nconst isChar = (char) => Boolean(char) && R_CHAR.test(char);\n\n/**\n * @template T\n * @param {T} object\n * @returns {T extends Document ? true : false}\n */\nconst isDocument = (object) => object?.nodeType === Node.DOCUMENT_NODE;\n\n/**\n * @template T\n * @param {T} object\n * @returns {T extends Element ? true: false}\n */\nconst isElement = (object) => object?.nodeType === Node.ELEMENT_NODE;\n\n/**\n * @param {Node} node\n */\nconst isQueryableNode = (node) => QUERYABLE_NODE_TYPES.includes(node.nodeType);\n\n/**\n * @param {Element} [el]\n */\nconst isRootElement = (el) => el && R_ROOT_ELEMENT.test(el.nodeName || \"\");\n\n/**\n * @param {Element} el\n */\nconst isShadowRoot = (el) => el.nodeType === Node.DOCUMENT_FRAGMENT_NODE && Boolean(el.host);\n\n/**\n * @template T\n * @param {T} object\n * @returns {T extends Window ? true : false}\n */\nconst isWindow = (object) => object?.window === object && object.constructor.name === \"Window\";\n\n/**\n * @param {string} [char]\n */\nconst isWhiteSpace = (char) => Boolean(char) && R_HORIZONTAL_WHITESPACE.test(char);\n\n/**\n * @param {string} pseudoClass\n * @param {(node: Node) => NodeValue} getContent\n */\nconst makePatternBasedPseudoClass = (pseudoClass, getContent) => {\n    return (content) => {\n        let regex;\n        try {\n            regex = parseRegExp(content);\n        } catch (err) {\n            throw selectorError(pseudoClass, err.message);\n        }\n        if (regex instanceof RegExp) {\n            return function containsRegExp(node) {\n                return regex.test(String(getContent(node)));\n            };\n        } else {\n            const lowerContent = content.toLowerCase();\n            return function containsString(node) {\n                return getStringContent(String(getContent(node)))\n                    .toLowerCase()\n                    .includes(lowerContent);\n            };\n        }\n    };\n};\n\n/**\n *\n * @param {string | (node: Node, index: number, nodes: Node[]) => boolean} filter\n * @param {Node} node\n * @param {number} index\n * @param {Node[]} allNodes\n * @returns\n */\nconst matchFilter = (filter, nodes, index) => {\n    const node = nodes[index];\n    if (typeof filter === \"function\") {\n        return filter(node, index, nodes);\n    } else {\n        return node.matches?.(String(filter));\n    }\n};\n\n/**\n * @param {string} query\n * @param {number} width\n * @param {number} height\n */\nconst matchesQuery = (query, width, height) =>\n    query\n        .toLowerCase()\n        .split(/\\s*,\\s*/)\n        .some((orPart) =>\n            orPart\n                .split(/\\s*\\band\\b\\s*/)\n                .every((andPart) => matchesQueryPart(andPart, width, height))\n        );\n\n/**\n * @param {string} query\n * @param {number} width\n * @param {number} height\n */\nconst matchesQueryPart = (query, width, height) => {\n    const [, key, value] = query.match(/\\(\\s*([\\w-]+)\\s*:\\s*(.+)\\s*\\)/) || [];\n    let result = false;\n    if (key) {\n        switch (key) {\n            case \"display-mode\": {\n                result = value === mockedMatchMedia.DISPLAY_MODE;\n                break;\n            }\n            case \"max-height\": {\n                result = height <= $parseFloat(value);\n                break;\n            }\n            case \"max-width\": {\n                result = width <= $parseFloat(value);\n                break;\n            }\n            case \"min-height\": {\n                result = height >= $parseFloat(value);\n                break;\n            }\n            case \"min-width\": {\n                result = width >= $parseFloat(value);\n                break;\n            }\n            case \"orientation\": {\n                result = value === \"landscape\" ? width > height : width < height;\n                break;\n            }\n            case \"pointer\": {\n                switch (value) {\n                    case \"coarse\": {\n                        result = globalThis.ontouchstart !== undefined;\n                        break;\n                    }\n                    case \"fine\": {\n                        result = globalThis.ontouchstart === undefined;\n                        break;\n                    }\n                }\n                break;\n            }\n            case \"prefers-color-scheme\": {\n                result = value === mockedMatchMedia.COLOR_SCHEME;\n                break;\n            }\n            case \"prefers-reduced-motion\": {\n                result = value === mockedMatchMedia.REDUCED_MOTION;\n                break;\n            }\n        }\n    }\n\n    return query.startsWith(\"not\") ? !result : result;\n};\n\n/**\n * @template T\n * @param {T} value\n * @param {(keyof T)[]} propsA\n * @param {(keyof T)[]} propsB\n * @returns {[number, number]}\n */\nconst parseNumberTuple = (value, propsA, propsB) => {\n    let result = [];\n    if (value && typeof value === \"object\") {\n        if (isIterable(value)) {\n            [result[0], result[1]] = [...value];\n        } else {\n            for (const prop of propsA) {\n                result[0] ??= value[prop];\n            }\n            for (const prop of propsB) {\n                result[1] ??= value[prop];\n            }\n        }\n    } else {\n        result = [value, value];\n    }\n    return result.map($parseFloat);\n};\n\n/**\n * Parses a given selector string into a list of selector groups.\n *\n * - the return value is a list of selector `group` objects (representing comma-separated\n *  selectors);\n * - a `group` is composed of one or more `part` objects (representing space-separated\n *  selector parts inside of a group);\n * - a `part` is composed of a base selector (string) and zero or more 'filters' (predicates).\n *\n * @param {string} selector\n */\nconst parseSelector = (selector) => {\n    /**\n     * @param {string} selector\n     */\n    const addToSelector = (selector) => {\n        registerChar = false;\n        const index = currentPart.length - 1;\n        if (typeof currentPart[index] === \"string\") {\n            currentPart[index] += selector;\n        } else {\n            currentPart.push(selector);\n        }\n    };\n\n    /** @type {(string | ReturnType<PseudoClassPredicateBuilder>)[]} */\n    const firstPart = [\"\"];\n    const firstGroup = [firstPart];\n    const groups = [firstGroup];\n    const parens = [0, 0];\n\n    let currentGroup = groups.at(-1);\n    let currentPart = currentGroup.at(-1);\n    let currentPseudo = null;\n    let currentQuote = null;\n    let registerChar = true;\n\n    for (let i = 0; i < selector.length; i++) {\n        const char = selector[i];\n        registerChar = true;\n        switch (char) {\n            // Group separator (comma)\n            case \",\": {\n                if (!currentQuote && !currentPseudo) {\n                    groups.push([[\"\"]]);\n                    currentGroup = groups.at(-1);\n                    currentPart = currentGroup.at(-1);\n                    registerChar = false;\n                }\n                break;\n            }\n            // Part separator (white space)\n            case \" \":\n            case \"\\t\":\n            case \"\\n\":\n            case \"\\r\":\n            case \"\\f\":\n            case \"\\v\": {\n                if (!currentQuote && !currentPseudo) {\n                    if (currentPart[0] || currentPart.length > 1) {\n                        // Only push new part if the current one is not empty\n                        // (has at least 1 character OR 1 pseudo-class filter)\n                        currentGroup.push([\"\"]);\n                        currentPart = currentGroup.at(-1);\n                    }\n                    registerChar = false;\n                }\n                break;\n            }\n            // Quote delimiters\n            case `'`:\n            case `\"`: {\n                if (char === currentQuote) {\n                    currentQuote = null;\n                } else if (!currentQuote) {\n                    currentQuote = char;\n                }\n                break;\n            }\n            // Combinators\n            case \">\":\n            case \"+\":\n            case \"~\": {\n                if (!currentQuote && !currentPseudo) {\n                    while (isWhiteSpace(selector[i + 1])) {\n                        i++;\n                    }\n                    addToSelector(char);\n                }\n                break;\n            }\n            // Pseudo classes\n            case \":\": {\n                if (!currentQuote && !currentPseudo) {\n                    let pseudo = \"\";\n                    while (isChar(selector[i + 1])) {\n                        pseudo += selector[++i];\n                    }\n                    if (customPseudoClasses.has(pseudo)) {\n                        if (selector[i + 1] === \"(\") {\n                            parens[0]++;\n                            i++;\n                            registerChar = false;\n                        }\n                        currentPseudo = [pseudo, \"\"];\n                    } else {\n                        addToSelector(char + pseudo);\n                    }\n                }\n                break;\n            }\n            // Parentheses\n            case \"(\": {\n                if (!currentQuote) {\n                    parens[0]++;\n                }\n                break;\n            }\n            case \")\": {\n                if (!currentQuote) {\n                    parens[1]++;\n                }\n                break;\n            }\n        }\n\n        if (currentPseudo) {\n            if (parens[0] === parens[1]) {\n                const [pseudo, content] = currentPseudo;\n                const makeFilter = customPseudoClasses.get(pseudo);\n                if (pseudo === \"iframe\" && !currentPart[0].startsWith(\"iframe\")) {\n                    // Special case: to optimise the \":iframe\" pseudo class, we\n                    // always select actual `iframe` elements.\n                    // Note that this may create \"impossible\" tag names (like \"iframediv\")\n                    // but this pseudo won't work on non-iframe elements anyway.\n                    currentPart[0] = `iframe${currentPart[0]}`;\n                }\n                currentPart.push(makeFilter(getStringContent(content)));\n                currentPseudo = null;\n            } else if (registerChar) {\n                currentPseudo[1] += selector[i];\n            }\n        } else if (registerChar) {\n            addToSelector(selector[i]);\n        }\n    }\n\n    return groups;\n};\n\n/**\n * @param {string} xmlString\n * @param {\"html\" | \"xml\"} type\n */\nconst parseXml = (xmlString, type) => {\n    const wrapperTag = type === \"html\" ? \"body\" : \"templates\";\n    const document = parser.parseFromString(\n        `<${wrapperTag}>${xmlString}</${wrapperTag}>`,\n        `text/${type}`\n    );\n    if (document.getElementsByTagName(\"parsererror\").length) {\n        const trimmed = xmlString.length > 80 ? xmlString.slice(0, 80) + \"\u2026\" : xmlString;\n        throw new HootDomError(\n            `error while parsing ${trimmed}: ${getNodeText(\n                document.getElementsByTagName(\"parsererror\")[0]\n            )}`\n        );\n    }\n    return document.getElementsByTagName(wrapperTag)[0].childNodes;\n};\n\n/**\n * Converts a CSS pixel value to a number, removing the 'px' part.\n *\n * @param {string} val\n */\nconst pixelValueToNumber = (val) => $parseFloat(val.endsWith(\"px\") ? val.slice(0, -2) : val);\n\n/**\n * @param {Node[]} nodes\n * @param {string} selector\n */\nconst queryWithCustomSelector = (nodes, selector) => {\n    const selectorGroups = parseSelector(selector);\n    const foundNodes = [];\n    for (const selectorParts of selectorGroups) {\n        let groupNodes = nodes;\n        for (const [partSelector, ...filters] of selectorParts) {\n            let baseSelector = partSelector;\n            let nodeGetter;\n            switch (baseSelector[0]) {\n                case \"+\": {\n                    nodeGetter = NEXT_SIBLING;\n                    break;\n                }\n                case \">\": {\n                    nodeGetter = DIRECT_CHILDREN;\n                    break;\n                }\n                case \"~\": {\n                    nodeGetter = NEXT_SIBLINGS;\n                    break;\n                }\n            }\n\n            // Slices modifier (if any)\n            if (nodeGetter) {\n                baseSelector = baseSelector.slice(1);\n            }\n\n            // Retrieve matching nodes and apply filters\n            const getNodes = nodeGetter || DESCENDANTS;\n            let currentGroupNodes = groupNodes.flatMap((node) => getNodes(node, baseSelector));\n\n            // Filter/replace nodes based on custom pseudo-classes\n            const pseudosReturningNode = new Set();\n            for (const filter of filters) {\n                const filteredGroupNodes = [];\n                for (let i = 0; i < currentGroupNodes.length; i++) {\n                    const result = matchFilter(filter, currentGroupNodes, i);\n                    if (result === true) {\n                        filteredGroupNodes.push(currentGroupNodes[i]);\n                    } else if (result) {\n                        filteredGroupNodes.push(result);\n                        pseudosReturningNode.add(filter.name);\n                    }\n                }\n\n                if (pseudosReturningNode.size > 1) {\n                    const pseudoList = [...pseudosReturningNode];\n                    throw selectorError(\n                        pseudoList[0],\n                        `cannot use multiple pseudo-classes returning nodes (${and(pseudoList)})`\n                    );\n                }\n\n                currentGroupNodes = filteredGroupNodes;\n            }\n\n            groupNodes = currentGroupNodes;\n        }\n\n        foundNodes.push(...groupNodes);\n    }\n\n    return filterUniqueNodes(foundNodes);\n};\n\n/**\n * @param {string} pseudoClass\n * @param {string} message\n */\nconst selectorError = (pseudoClass, message) =>\n    new HootDomError(`invalid selector \\`:${pseudoClass}\\`: ${message}`);\n\n// Regexes\nconst R_CHAR = /[\\w-]/;\nconst R_QUOTE_CONTENT = /^\\s*(['\"])?([^]*?)\\1\\s*$/;\nconst R_ROOT_ELEMENT = /^(HTML|HEAD|BODY)$/;\n/**\n * \\s without \\n and \\v\n */\nconst R_HORIZONTAL_WHITESPACE =\n    /[\\r\\t\\f \\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]+/g;\n\nconst QUERYABLE_NODE_TYPES = [Node.ELEMENT_NODE, Node.DOCUMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE];\n\nconst parser = new DOMParser();\n\n// Node getters\n\n/** @type {NodeGetter} */\nconst DIRECT_CHILDREN = (node, selector) => {\n    const children = [];\n    for (const childNode of node.childNodes) {\n        if (childNode.matches?.(selector)) {\n            children.push(childNode);\n        }\n    }\n    return children;\n};\n\n/** @type {NodeGetter} */\nconst DESCENDANTS = (node, selector) => [...(node.querySelectorAll?.(selector || \"*\") || [])];\n\n/** @type {NodeGetter} */\nconst NEXT_SIBLING = (node, selector) => {\n    const sibling = node.nextElementSibling;\n    return sibling?.matches?.(selector) ? [sibling] : [];\n};\n\n/** @type {NodeGetter} */\nconst NEXT_SIBLINGS = (node, selector) => {\n    const siblings = [];\n    while ((node = node.nextElementSibling)) {\n        if (node.matches?.(selector)) {\n            siblings.push(node);\n        }\n    }\n    return siblings;\n};\n\n/** @type {Map<HTMLElement, { callbacks: Set<MutationCallback>, observer: MutationObserver }>} */\nconst observers = new Map();\nconst currentDimensions = {\n    width: innerWidth,\n    height: innerHeight,\n};\nlet getDefaultRoot = () => document;\n\n//-----------------------------------------------------------------------------\n// Pseudo classes\n//-----------------------------------------------------------------------------\n\n/** @type {Map<string, PseudoClassPredicateBuilder>} */\nconst customPseudoClasses = new Map();\n\ncustomPseudoClasses\n    .set(\"contains\", makePatternBasedPseudoClass(\"contains\", getNodeText))\n    .set(\"displayed\", () => {\n        return function displayed(node) {\n            return isNodeDisplayed(node);\n        };\n    })\n    .set(\"empty\", () => {\n        return function empty(node) {\n            return isEmpty(node);\n        };\n    })\n    .set(\"eq\", (content) => {\n        const index = $parseInt(content);\n        if (!$isInteger(index)) {\n            throw selectorError(\"eq\", `expected index to be an integer (got ${content})`);\n        }\n        return function eq(node, i, nodes) {\n            return index < 0 ? i === nodes.length + index : i === index;\n        };\n    })\n    .set(\"first\", () => {\n        return function first(node, i) {\n            return i === 0;\n        };\n    })\n    .set(\"focusable\", () => {\n        return function focusable(node) {\n            return isNodeFocusable(node);\n        };\n    })\n    .set(\"has\", (content) => {\n        return function has(node) {\n            return Boolean(queryAll(content, { root: node }).length);\n        };\n    })\n    .set(\"hidden\", () => {\n        return function hidden(node) {\n            return !isNodeVisible(node);\n        };\n    })\n    .set(\"iframe\", () => {\n        return function iframe(node) {\n            // Note: should only apply on `iframe` elements\n            /** @see parseSelector */\n            const doc = node.contentDocument;\n            return doc && doc.readyState !== \"loading\" ? doc : false;\n        };\n    })\n    .set(\"last\", () => {\n        return function last(node, i, nodes) {\n            return i === nodes.length - 1;\n        };\n    })\n    .set(\"not\", (content) => {\n        return function not(node) {\n            return !matches(node, content);\n        };\n    })\n    .set(\"only\", () => {\n        return function only(node, i, nodes) {\n            return nodes.length === 1;\n        };\n    })\n    .set(\"scrollable\", () => {\n        return function scrollable(node) {\n            return isNodeScrollable(node);\n        };\n    })\n    .set(\"selected\", () => {\n        return function selected(node) {\n            return Boolean(node.selected);\n        };\n    })\n    .set(\"shadow\", () => {\n        return function shadow(node) {\n            return node.shadowRoot || false;\n        };\n    })\n    .set(\"value\", makePatternBasedPseudoClass(\"value\", getNodeValue))\n    .set(\"visible\", () => {\n        return function visible(node) {\n            return isNodeVisible(node);\n        };\n    });\n\nconst rCustomPseudoClass = compilePseudoClassRegex();\n\n//-----------------------------------------------------------------------------\n// Internal exports (inside Hoot/Hoot-DOM)\n//-----------------------------------------------------------------------------\n\nexport function cleanupDOM() {\n    // Dimensions\n    currentDimensions.width = innerWidth;\n    currentDimensions.height = innerHeight;\n\n    // Observers\n    const remainingObservers = observers.size;\n    if (remainingObservers) {\n        for (const { observer } of observers.values()) {\n            observer.disconnect();\n        }\n        observers.clear();\n    }\n}\n\n/**\n * @param {Node | () => Node} node\n */\nexport function defineRootNode(node) {\n    if (typeof node === \"function\") {\n        getDefaultRoot = node;\n    } else if (node) {\n        getDefaultRoot = () => node;\n    } else {\n        getDefaultRoot = () => document;\n    }\n}\n\nexport function getCurrentDimensions() {\n    return currentDimensions;\n}\n\nexport function getDefaultRootNode() {\n    return getDefaultRoot();\n}\n\n/**\n * @param {Node} [node]\n * @returns {Document}\n */\nexport function getDocument(node) {\n    node ||= getDefaultRoot();\n    return isDocument(node) ? node : node.ownerDocument || document;\n}\n\n/**\n * @param {Node} node\n * @param {string} attribute\n * @returns {string | null}\n */\nexport function getNodeAttribute(node, attribute) {\n    return node.getAttribute?.(attribute) ?? null;\n}\n\n/**\n * @param {Node} node\n * @returns {NodeValue}\n */\nexport function getNodeValue(node) {\n    switch (node.type) {\n        case \"checkbox\":\n        case \"radio\":\n            return node.checked;\n        case \"file\":\n            return [...node.files];\n        case \"number\":\n        case \"range\":\n            return node.valueAsNumber;\n        case \"date\":\n        case \"datetime-local\":\n        case \"month\":\n        case \"time\":\n        case \"week\":\n            return node.valueAsDate.toISOString();\n    }\n    return node.value;\n}\n\n/**\n * @param {Node} node\n * @param {QueryRectOptions} [options]\n */\nexport function getNodeRect(node, options) {\n    if (!isElement(node)) {\n        return new DOMRect();\n    }\n\n    /** @type {DOMRect} */\n    const rect = node.getBoundingClientRect();\n    const parentFrame = getParentFrame(node);\n    if (parentFrame) {\n        const parentRect = getNodeRect(parentFrame);\n        rect.x -= parentRect.x;\n        rect.y -= parentRect.y;\n    }\n\n    if (!options?.trimPadding) {\n        return rect;\n    }\n\n    const style = getStyle(node);\n    const { x, y, width, height } = rect;\n    const [pl, pr, pt, pb] = [\"left\", \"right\", \"top\", \"bottom\"].map((side) =>\n        pixelValueToNumber(style.getPropertyValue(`padding-${side}`))\n    );\n\n    return new DOMRect(x + pl, y + pt, width - (pl + pr), height - (pt + pb));\n}\n\n/**\n * @param {Node} node\n * @param {QueryTextOptions} [options]\n * @returns {string}\n */\nexport function getNodeText(node, options) {\n    let content;\n    if (typeof node.innerText === \"string\") {\n        content = node.innerText;\n    } else {\n        content = node.textContent;\n    }\n    if (options?.raw) {\n        return content;\n    }\n    return content.replace(R_HORIZONTAL_WHITESPACE, \" \").trim();\n}\n\n/**\n * @template {Node} T\n * @param {T} node\n * @returns {T extends Element ? CSSStyleDeclaration : null}\n */\nexport function getStyle(node) {\n    return isElement(node) ? getComputedStyle(node) : null;\n}\n\n/**\n * @param {Node} [node]\n * @returns {Window}\n */\nexport function getWindow(node) {\n    return getDocument(node).defaultView;\n}\n\n/**\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isCheckable(node) {\n    switch (getTag(node)) {\n        case \"input\":\n            return node.type === \"checkbox\" || node.type === \"radio\";\n        case \"label\":\n            return isCheckable(node.control);\n        default:\n            return false;\n    }\n}\n\n/**\n * @param {unknown} value\n * @returns {boolean}\n */\nexport function isEmpty(value) {\n    if (!value) {\n        return true;\n    }\n    if (typeof value === \"object\") {\n        if (isNode(value)) {\n            return isEmpty(getNodeContent(value));\n        }\n        if (!isIterable(value)) {\n            value = $keys(value);\n        }\n        return [...value].length === 0;\n    }\n    return false;\n}\n\n/**\n * Returns whether the given object is an {@link EventTarget}.\n *\n * @template T\n * @param {T} object\n * @returns {T extends EventTarget ? true : false}\n * @example\n *  isEventTarget(window); // true\n * @example\n *  isEventTarget(new App()); // false\n */\nexport function isEventTarget(object) {\n    return object && typeof object.addEventListener === \"function\";\n}\n\n/**\n * Returns whether the given object is a {@link Node} object.\n * Note that it is independant from the {@link Node} class itself to support\n * cross-window checks.\n *\n * @template T\n * @param {T} object\n * @returns {T extends Node ? true : false}\n */\nexport function isNode(object) {\n    return object && typeof object.nodeType === \"number\" && typeof object.nodeName === \"string\";\n}\n\n/**\n * @param {Node} node\n */\nexport function isNodeCssVisible(node) {\n    const element = ensureElement(node);\n    if (element === getDefaultRoot() || isRootElement(element)) {\n        return true;\n    }\n    const style = getStyle(element);\n    if (style?.visibility === \"hidden\" || style?.opacity === \"0\") {\n        return false;\n    }\n    const parent = element.parentNode;\n    return !parent || isNodeCssVisible(isShadowRoot(parent) ? parent.host : parent);\n}\n\n/**\n * @param {Window | Node} node\n */\nexport function isNodeDisplayed(node) {\n    const element = ensureElement(node);\n    if (!isInDOM(element)) {\n        return false;\n    }\n    if (isRootElement(element) || element.offsetParent || element.closest(\"svg\")) {\n        return true;\n    }\n    // `position=fixed` elements in Chrome do not have an `offsetParent`\n    return !isFirefox() && getStyle(element)?.position === \"fixed\";\n}\n\n/**\n * @param {Node} node\n * @param {FocusableOptions} node\n */\nexport function isNodeFocusable(node, options) {\n    return (\n        isNodeDisplayed(node) &&\n        node.matches?.(FOCUSABLE_SELECTOR) &&\n        (!options?.tabbable || node.tabIndex >= 0)\n    );\n}\n\n/**\n * @param {Window | Node} node\n */\nexport function isNodeInViewPort(node) {\n    const element = ensureElement(node);\n    const { x, y } = getNodeRect(element);\n\n    return y > 0 && y < currentDimensions.height && x > 0 && x < currentDimensions.width;\n}\n\n/**\n * @param {Window | Node} node\n * @param {\"x\" | \"y\"} [axis]\n */\nexport function isNodeScrollable(node, axis) {\n    if (!isElement(node)) {\n        return false;\n    }\n    const [scrollProp, sizeProp] =\n        axis === \"x\" ? [\"scrollWidth\", \"clientWidth\"] : [\"scrollHeight\", \"clientHeight\"];\n    if (node[scrollProp] > node[sizeProp]) {\n        const overflow = getStyle(node).getPropertyValue(\"overflow\");\n        if (/\\bauto\\b|\\bscroll\\b/.test(overflow)) {\n            return true;\n        }\n    }\n    return false;\n}\n\n/**\n * @param {Window | Node} node\n */\nexport function isNodeVisible(node) {\n    const element = ensureElement(node);\n\n    // Must be displayed and not hidden by CSS\n    if (!isNodeDisplayed(element) || !isNodeCssVisible(element)) {\n        return false;\n    }\n\n    let visible = false;\n\n    // Check size (width & height)\n    const { width, height } = getNodeRect(element);\n    visible = width > 0 && height > 0;\n\n    // Check content (if display=contents)\n    if (!visible && getStyle(element)?.display === \"contents\") {\n        for (const child of element.childNodes) {\n            if (isNodeVisible(child)) {\n                return true;\n            }\n        }\n    }\n\n    return visible;\n}\n\n/**\n * @type {typeof matchMedia}\n */\nexport function mockedMatchMedia(query) {\n    let onchange = null;\n    return {\n        addEventListener: (type, callback) => window.addEventListener(\"resize\", callback),\n        get matches() {\n            return matchesQuery(query, window.innerWidth, window.innerHeight);\n        },\n        media: query,\n        get onchange() {\n            return onchange;\n        },\n        set onchange(value) {\n            value ||= null;\n            if (value) {\n                window.addEventListener(\"resize\", value);\n            } else {\n                window.removeEventListener(\"resize\", onchange);\n            }\n            onchange = value;\n        },\n        removeEventListener: (type, callback) => window.removeEventListener(\"resize\", callback),\n    };\n}\n\nmockedMatchMedia.COLOR_SCHEME = \"light\";\nmockedMatchMedia.DISPLAY_MODE = \"browser\";\nmockedMatchMedia.REDUCED_MOTION = \"reduce\";\n\n/**\n * @param {Dimensions} dimensions\n * @returns {[number, number]}\n */\nexport function parseDimensions(dimensions) {\n    return parseNumberTuple(dimensions, [\"width\", \"w\"], [\"height\", \"h\"]);\n}\n\n/**\n * @param {Position} position\n * @returns {[number, number]}\n */\nexport function parsePosition(position) {\n    return parseNumberTuple(\n        position,\n        [\"x\", \"left\", \"clientX\", \"pageX\", \"screenX\"],\n        [\"y\", \"top\", \"clientY\", \"pageY\", \"screenY\"]\n    );\n}\n\n/**\n * @param {number} width\n * @param {number} height\n */\nexport function setDimensions(width, height) {\n    const defaultRoot = getDefaultRoot();\n    if (!$isNaN(width)) {\n        currentDimensions.width = width;\n        defaultRoot.style?.setProperty(\"width\", `${width}px`, \"important\");\n    }\n    if (!$isNaN(height)) {\n        currentDimensions.height = height;\n        defaultRoot.style?.setProperty(\"height\", `${height}px`, \"important\");\n    }\n}\n\n/**\n * @param {Node} node\n * @param {{ object?: boolean }} [options]\n * @returns {string | string[]}\n */\nexport function toSelector(node, options) {\n    const parts = {\n        tag: node.nodeName.toLowerCase(),\n    };\n    if (node.id) {\n        parts.id = `#${node.id}`;\n    }\n    if (node.classList?.length) {\n        parts.class = `.${[...node.classList].join(\".\")}`;\n    }\n    return options?.object ? parts : $values(parts).join(\"\");\n}\n\n// Following selector is based on this spec:\n// https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex\nexport const FOCUSABLE_SELECTOR = [\n    \"a[href]\",\n    \"area[href]\",\n    \"button:enabled\",\n    \"details > summary:first-of-type\",\n    \"iframe\",\n    \"input:enabled\",\n    \"select:enabled\",\n    \"textarea:enabled\",\n    \"[tabindex]\",\n    \"[contenteditable=true]\",\n].join(\",\");\n\n//-----------------------------------------------------------------------------\n// Exports\n//-----------------------------------------------------------------------------\n\n/**\n * Returns a standardized representation of the given `string` value as a human-readable\n * XML string template (or HTML if the `type` option is `\"html\"`).\n *\n * @param {string} value\n * @param {FormatXmlOptions} [options]\n * @returns {string}\n */\nexport function formatXml(value, options) {\n    const nodes = parseXml(value, options?.type || \"xml\");\n    const layers = extractLayers(nodes, 0, options?.keepInlineTextNodes ?? false);\n    return generateStringFromLayers(layers, options?.tabSize ?? 4);\n}\n\n/**\n * Returns the active element in the given document (or in the owner document of\n * the given node).\n *\n * @param {Node} [node]\n */\nexport function getActiveElement(node) {\n    const { activeElement } = getDocument(node);\n    if (activeElement.contentDocument) {\n        return getActiveElement(activeElement.contentDocument);\n    }\n    if (activeElement.shadowRoot) {\n        return activeElement.shadowRoot.activeElement;\n    }\n    return activeElement;\n}\n\n/**\n * Returns the list of focusable elements in the given parent, sorted by their `tabIndex`\n * property.\n *\n * @see {@link isFocusable} for more information\n * @param {FocusableOptions} [options]\n * @returns {Element[]}\n * @example\n *  getFocusableElements();\n */\nexport function getFocusableElements(options) {\n    const parent = options?.root || getDefaultRoot();\n    if (typeof parent.querySelectorAll !== \"function\") {\n        return [];\n    }\n    const byTabIndex = {};\n    for (const element of parent.querySelectorAll(FOCUSABLE_SELECTOR)) {\n        const { tabIndex } = element;\n        if ((options?.tabbable && tabIndex < 0) || !isNodeDisplayed(element)) {\n            continue;\n        }\n        if (!byTabIndex[tabIndex]) {\n            byTabIndex[tabIndex] = [];\n        }\n        byTabIndex[tabIndex].push(element);\n    }\n    const withTabIndexZero = byTabIndex[0] || [];\n    delete byTabIndex[0];\n    return [...$values(byTabIndex).flat(), ...withTabIndexZero];\n}\n\n/**\n * Returns the next focusable element after the current active element if it is\n * contained in the given parent.\n *\n * @see {@link getFocusableElements}\n * @param {FocusableOptions} [options]\n * @returns {Element | null}\n * @example\n *  getPreviousFocusableElement();\n */\nexport function getNextFocusableElement(options) {\n    const parent = options?.root || getDefaultRoot();\n    const focusableEls = getFocusableElements(parent, options);\n    const index = focusableEls.indexOf(getActiveElement(parent));\n    return focusableEls[index + 1] || null;\n}\n\n/**\n * Returns the parent `<iframe>` of a given node (if any).\n *\n * @param {Node} node\n * @returns {HTMLIFrameElement | null}\n */\nexport function getParentFrame(node) {\n    const nodeDocument = node.ownerDocument;\n    const view = nodeDocument.defaultView;\n    if (view !== view.parent) {\n        for (const iframe of view.parent.document.getElementsByTagName(\"iframe\")) {\n            if (iframe.contentDocument === nodeDocument) {\n                return iframe;\n            }\n        }\n    }\n    return null;\n}\n\n/**\n * Returns the previous focusable element before the current active element if it is\n * contained in the given parent.\n *\n * @see {@link getFocusableElements}\n * @param {FocusableOptions} [options]\n * @returns {Element | null}\n * @example\n *  getPreviousFocusableElement();\n */\nexport function getPreviousFocusableElement(options) {\n    const parent = options?.root || getDefaultRoot();\n    const focusableEls = getFocusableElements(parent, options);\n    const index = focusableEls.indexOf(getActiveElement(parent));\n    return index < 0 ? focusableEls.at(-1) : focusableEls[index - 1] || null;\n}\n\n/**\n * Checks whether a target is displayed, meaning that it has an offset parent and\n * is contained in the current document.\n *\n * Note that it does not mean that the target is \"visible\" (it can still be hidden\n * by CSS properties such as `width`, `opacity`, `visiblity`, etc.).\n *\n * @param {Target} target\n * @returns {boolean}\n */\nexport function isDisplayed(target) {\n    return queryAll(target, { displayed: true }).length > 0;\n}\n\n/**\n * Returns whether the given node is editable, meaning that it is an `\":enabled\"`\n * `<input>` or `<textarea>` {@link Element};\n *\n * Note: this does **NOT** support elements with `contenteditable=\"true\"`.\n *\n * @param {Node} node\n * @returns {boolean}\n * @example\n *  isEditable(document.querySelector(\"input\")); // true\n * @example\n *  isEditable(document.body); // false\n */\nexport function isEditable(node) {\n    return (\n        isElement(node) &&\n        !node.matches?.(\":disabled\") &&\n        [\"input\", \"textarea\"].includes(getTag(node))\n    );\n}\n\n/**\n * Returns whether an element is focusable. Focusable elements are either:\n * - `<a>` or `<area>` elements with an `href` attribute;\n * - *enabled* `<button>`, `<input>`, `<select>` and `<textarea>` elements;\n * - `<iframe>` elements;\n * - any element with its `contenteditable` attribute set to `\"true\"`.\n *\n * A focusable element must also not have a `tabIndex` property set to less than 0.\n *\n * @see {@link FOCUSABLE_SELECTOR}\n * @param {Target} target\n * @param {FocusableOptions} [options]\n * @returns {boolean}\n */\nexport function isFocusable(target, options) {\n    const nodes = queryAll(...arguments);\n    return nodes.length && nodes.every((node) => isNodeFocusable(node, options));\n}\n\n/**\n * Returns whether the given target is contained in the current root document.\n *\n * @param {Window | Node} target\n * @returns {boolean}\n * @example\n *  isInDOM(queryFirst(\"div\")); // true\n * @example\n *  isInDOM(document.createElement(\"div\")); // Not attached -> false\n */\nexport function isInDOM(target) {\n    return ensureElement(target)?.isConnected;\n}\n\n/**\n * Checks whether a target is *at least partially* visible in the current viewport.\n *\n * @param {Target} target\n * @returns {boolean}\n */\nexport function isInViewPort(target) {\n    return queryAll(target, { viewPort: true }).length > 0;\n}\n\n/**\n * Returns whether an element is scrollable.\n *\n * @param {Target} target\n * @param {\"x\" | \"y\"} [axis]\n * @returns {boolean}\n */\nexport function isScrollable(target, axis) {\n    const nodes = queryAll(target);\n    return nodes.length && nodes.every((node) => isNodeScrollable(node, axis));\n}\n\n/**\n * Checks whether a target is visible, meaning that it is \"displayed\" (see {@link isDisplayed}),\n * has a non-zero width and height, and is not hidden by \"opacity\" or \"visibility\"\n * CSS properties.\n *\n * Note that it does not account for:\n *  - the position of the target in the viewport (e.g. negative x/y coordinates)\n *  - the color of the target (e.g. transparent text with no background).\n *\n * @param {Target} target\n * @returns {boolean}\n */\nexport function isVisible(target) {\n    return queryAll(target, { visible: true }).length > 0;\n}\n\n/**\n * Equivalent to the native `node.matches(selector)`, with a few differences:\n * - it can take any {@link Target} (strings, nodes and iterable of nodes);\n * - it supports custom pseudo-classes, such as \":contains\" or \":visible\".\n *\n * @param {Target} target\n * @param {string} selector\n * @returns {boolean}\n * @example\n *  matches(\"input[name=surname]\", \":value(John)\");\n * @example\n *  matches(buttonEl, \":contains(Submit)\");\n */\nexport function matches(target, selector) {\n    return elementsMatch(queryAll(target), selector);\n}\n\n/**\n * Listens for DOM mutations on a given target.\n *\n * This helper has 2 main advantages over directly calling the native MutationObserver:\n * - it ensures a single observer is created for a given target, even if multiple\n *  callbacks are registered;\n * - it keeps track of these observers, which allows to check whether an observer\n *  is still running while it should not, and to disconnect all running observers\n *  at once.\n *\n * @param {HTMLElement} target\n * @param {MutationCallback} callback\n */\nexport function observe(target, callback) {\n    if (observers.has(target)) {\n        observers.get(target).callbacks.add(callback);\n    } else {\n        const callbacks = new Set([callback]);\n        const observer = new MutationObserver((mutations, observer) => {\n            for (const callback of callbacks) {\n                callback(mutations, observer);\n            }\n        });\n        observer.observe(target, {\n            attributes: true,\n            characterData: true,\n            childList: true,\n            subtree: true,\n        });\n        observers.set(target, { callbacks, observer });\n    }\n\n    return function disconnect() {\n        if (!observers.has(target)) {\n            return;\n        }\n        const { callbacks, observer } = observers.get(target);\n        callbacks.delete(callback);\n        if (!callbacks.size) {\n            observer.disconnect();\n            observers.delete(target);\n        }\n    };\n}\n\n/**\n * Returns a list of nodes matching the given {@link Target}.\n * This function can either be used as a **template literal tag** (only supports\n * string selector without options) or invoked the usual way.\n *\n * The target can be:\n * - a {@link Node} (or an iterable of nodes), or {@link Window} object;\n * - a {@link Document} object (which will be converted to its body);\n * - a string representing a *custom selector* (which will be queried in the `root` option);\n *\n * This function allows all string selectors supported by the native {@link Element.querySelector}\n * along with some additional custom pseudo-classes:\n *\n * - `:contains(text)`: matches nodes whose *content* matches the given *text*;\n *      * given *text* supports regular expression syntax (e.g. `:contains(/^foo.+/)`)\n *          and is case-insensitive;\n *      * given *text* will be matched against:\n *          - an `<input>`, `<textarea>` or `<select>` element's **value**;\n *          - or any other element's **inner text**.\n * - `:displayed`: matches nodes that are \"displayed\" (see {@link isDisplayed});\n * - `:empty`: matches nodes that have an empty *content* (**value** or **inner text**);\n * - `:eq(n)`: matches the *nth* node (0-based index);\n * - `:first`: matches the first node matching the selector (regardless of its actual\n *  DOM siblings);\n * - `:focusable`: matches nodes that can be focused (see {@link isFocusable});\n * - `:hidden`: matches nodes that are **not** \"visible\" (see {@link isVisible});\n * - `:iframe`: matches nodes that are `<iframe>` elements, and returns their `body`\n *  if it is ready;\n * - `:last`: matches the last node matching the selector (regardless of its actual\n *  DOM siblings);\n * - `:selected`: matches nodes that are selected (e.g. `<option>` elements);\n * - `:shadow`: matches nodes that have shadow roots, and returns their shadow root;\n * - `:scrollable`: matches nodes that are scrollable (see {@link isScrollable});\n * - `:visible`: matches nodes that are \"visible\" (see {@link isVisible});\n *\n * An `options` object can be specified to filter[1] the results:\n * - `displayed`: whether the nodes must be \"displayed\" (see {@link isDisplayed});\n * - `exact`: the exact number of nodes to match (throws an error if the number of\n *  nodes doesn't match);\n * - `focusable`: whether the nodes must be \"focusable\" (see {@link isFocusable});\n * - `root`: the root node to query the selector in (defaults to the current fixture);\n * - `viewPort`: whether the nodes must be partially visible in the current viewport\n *  (see {@link isInViewPort});\n * - `visible`: whether the nodes must be \"visible\" (see {@link isVisible}).\n *      * This option implies `displayed`\n *\n * [1] these filters (except for `exact` and `root`) achieve the same result as\n *  using their homonym pseudo-classes on the final group of the given selector\n *  string (e.g. ```queryAll`ul > li:visible`;``` = ```queryAll(\"ul > li\", { visible: true })```).\n *\n * @param {Target} target\n * @param {QueryOptions} [options]\n * @returns {Element[]}\n * @example\n *  // regular selectors\n *  queryAll`window`; // -> []\n *  queryAll`input#name`; // -> [input]\n *  queryAll`div`; // -> [div, div, ...]\n *  queryAll`ul > li`; // -> [li, li, ...]\n * @example\n *  // custom selectors\n *  queryAll`div:visible:contains(Lorem ipsum)`; // -> [div, div, ...]\n *  queryAll`div:visible:contains(${/^L\\w+\\si.*m$/})`; // -> [div, div, ...]\n *  queryAll`:focusable`; // -> [a, button, input, ...]\n *  queryAll`.o_iframe:iframe p`; // -> [p, p, ...] (inside iframe)\n *  queryAll`#editor:shadow div`; // -> [div, div, ...] (inside shadow DOM)\n * @example\n *  // with options\n *  queryAll(`div:first`, { exact: 1 }); // -> [div]\n *  queryAll(`div`, { root: queryOne`iframe` }); // -> [div, div, ...]\n *  // redundant, but possible\n *  queryAll(`button:visible`, { visible: true }); // -> [button, button, ...]\n */\nexport function queryAll(target, options) {\n    if (!target) {\n        return [];\n    }\n    if (target.raw) {\n        return queryAll(String.raw(...arguments));\n    }\n\n    const { exact, displayed, root, viewPort, visible } = options || {};\n\n    /** @type {Node[]} */\n    let nodes = [];\n    let selector;\n\n    if (typeof target === \"string\") {\n        nodes = root ? queryAll(root) : [getDefaultRoot()];\n        selector = target.trim();\n        // HTMLSelectElement is iterable \u00af\\_(\u30c4)_/\u00af\n    } else if (isIterable(target) && !isNode(target)) {\n        nodes = filterUniqueNodes(target);\n    } else {\n        nodes = filterUniqueNodes([target]);\n    }\n\n    if (selector && nodes.length) {\n        if (rCustomPseudoClass.test(selector)) {\n            nodes = queryWithCustomSelector(nodes, selector);\n        } else {\n            nodes = filterUniqueNodes(nodes.flatMap((node) => DESCENDANTS(node, selector)));\n        }\n    }\n\n    /** @type {string} */\n    let prefix, suffix;\n    if (visible + displayed > 1) {\n        throw new HootDomError(\n            `cannot use more than one visibility modifier ('visible' implies 'displayed')`\n        );\n    }\n    if (viewPort) {\n        nodes = nodes.filter(isNodeInViewPort);\n        suffix = \"in viewport\";\n    } else if (visible) {\n        nodes = nodes.filter(isNodeVisible);\n        prefix = \"visible\";\n    } else if (displayed) {\n        nodes = nodes.filter(isNodeDisplayed);\n        prefix = \"displayed\";\n    }\n\n    const count = nodes.length;\n    if ($isInteger(exact) && count !== exact) {\n        const s = count === 1 ? \"\" : \"s\";\n        const strPrefix = prefix ? `${prefix} ` : \"\";\n        const strSuffix = suffix ? ` ${suffix}` : \"\";\n        const strSelector = typeof target === \"string\" ? `(selector: \"${target}\")` : \"\";\n        throw new HootDomError(\n            `found ${count} ${strPrefix}node${s}${strSuffix} instead of ${exact} ${strSelector}`\n        );\n    }\n\n    return nodes;\n}\n\n/**\n * Performs a {@link queryOne} with the given arguments and returns the value of\n * the given *attribute* of the matching node.\n *\n * @param {Target} target\n * @param {string} attribute\n * @param {QueryOptions} [options]\n * @returns {string | null}\n */\nexport function queryAttribute(target, attribute, options) {\n    return getNodeAttribute(queryOne(target, options), attribute);\n}\n\n/**\n * Performs a {@link queryAll} with the given arguments and returns a list of the\n * *attribute values* of the matching nodes.\n *\n * @param {Target} target\n * @param {string} attribute\n * @param {QueryOptions} [options]\n * @returns {string[]}\n */\nexport function queryAllAttributes(target, attribute, options) {\n    return queryAll(target, options).map((node) => getNodeAttribute(node, attribute));\n}\n\n/**\n * Performs a {@link queryAll} with the given arguments and returns a list of the\n * *properties* of the matching nodes.\n *\n * @param {Target} target\n * @param {string} property\n * @param {QueryOptions} [options]\n * @returns {any[]}\n */\nexport function queryAllProperties(target, property, options) {\n    return queryAll(target, options).map((node) => node[property]);\n}\n\n/**\n * Performs a {@link queryAll} with the given arguments and returns a list of the\n * {@link DOMRect} of the matching nodes.\n *\n * There are a few differences with the native {@link Element.getBoundingClientRect}:\n * - rects take their positions relative to the top window element (instead of their\n *  parent `<iframe>` if any);\n * - they can be trimmed to remove padding with the `trimPadding` option.\n *\n * @param {Target} target\n * @param {QueryOptions & QueryRectOptions} [options]\n * @returns {DOMRect[]}\n */\nexport function queryAllRects(target, options) {\n    return queryAll(...arguments).map(getNodeRect);\n}\n\n/**\n * Performs a {@link queryAll} with the given arguments and returns a list of the\n * *texts* of the matching nodes.\n *\n * @param {Target} target\n * @param {QueryOptions & QueryTextOptions} [options]\n * @returns {string[]}\n */\nexport function queryAllTexts(target, options) {\n    return queryAll(...arguments).map((node) => getNodeText(node, options));\n}\n\n/**\n * Performs a {@link queryAll} with the given arguments and returns a list of the\n * *values* of the matching nodes.\n *\n * @param {Target} target\n * @param {QueryOptions} [options]\n * @returns {NodeValue[]}\n */\nexport function queryAllValues(target, options) {\n    return queryAll(...arguments).map(getNodeValue);\n}\n\n/**\n * Performs a {@link queryAll} with the given arguments and returns the first result\n * or `null`.\n *\n * @param {Target} target\n * @param {QueryOptions} options\n * @returns {Element | null}\n */\nexport function queryFirst(target, options) {\n    return queryAll(...arguments)[0] || null;\n}\n\n/**\n * Performs a {@link queryAll} with the given arguments, along with a forced `exact: 1`\n * option to ensure only one node matches the given {@link Target}.\n *\n * The returned value is a single node instead of a list of nodes.\n *\n * @param {Target} target\n * @param {Omit<QueryOptions, \"exact\">} [options]\n * @returns {Element}\n */\nexport function queryOne(target, options) {\n    if (target.raw) {\n        return queryOne(String.raw(...arguments));\n    }\n    if ($isInteger(options?.exact)) {\n        throw new HootDomError(\n            `cannot call \\`queryOne\\` with 'exact'=${options.exact}: did you mean to use \\`queryAll\\`?`\n        );\n    }\n    return queryAll(target, { ...options, exact: 1 })[0];\n}\n\n/**\n * Performs a {@link queryOne} with the given arguments and returns the {@link DOMRect}\n * of the matching node.\n *\n * There are a few differences with the native {@link Element.getBoundingClientRect}:\n * - rects take their positions relative to the top window element (instead of their\n *  parent `<iframe>` if any);\n * - they can be trimmed to remove padding with the `trimPadding` option.\n *\n * @param {Target} target\n * @param {QueryOptions & QueryRectOptions} [options]\n * @returns {DOMRect}\n */\nexport function queryRect(target, options) {\n    return getNodeRect(queryOne(...arguments), options);\n}\n\n/**\n * Performs a {@link queryOne} with the given arguments and returns the *text* of\n * the matching node.\n *\n * @param {Target} target\n * @param {QueryOptions & QueryTextOptions} [options]\n * @returns {string}\n */\nexport function queryText(target, options) {\n    return getNodeText(queryOne(...arguments), options);\n}\n\n/**\n * Performs a {@link queryOne} with the given arguments and returns the *value* of\n * the matching node.\n *\n * @param {Target} target\n * @param {QueryOptions} [options]\n * @returns {NodeValue}\n */\nexport function queryValue(target, options) {\n    return getNodeValue(queryOne(...arguments));\n}\n\n/**\n * Combination of {@link queryAll} and {@link waitUntil}: waits for a given target\n * to match elements in the DOM and returns the first matching node when it appears\n * (or immediately if it is already present).\n *\n * @see {@link queryAll}\n * @see {@link waitUntil}\n * @param {Target} target\n * @param {QueryOptions & WaitOptions} [options]\n * @returns {Deferred<Element>}\n * @example\n *  const button = await waitFor(`button`);\n *  button.click();\n */\nexport function waitFor(target, options) {\n    return waitUntil(() => queryFirst(...arguments), {\n        message: `Could not find elements matching \"${target}\" within %timeout% milliseconds`,\n        ...options,\n    });\n}\n\n/**\n * Opposite of {@link waitFor}: waits for a given target to disappear from the DOM\n * (resolves instantly if the selector is already missing).\n *\n * @see {@link waitFor}\n * @param {Target} target\n * @param {QueryOptions & WaitOptions} [options]\n * @returns {Promise<number>}\n * @example\n *  await waitForNone(`button`);\n */\nexport function waitForNone(target, options) {\n    let count = 0;\n    return waitUntil(\n        () => {\n            count = queryAll(...arguments).length;\n            return !count;\n        },\n        {\n            message: () =>\n                `Could still find ${count} elements matching \"${target}\" after %timeout% milliseconds`,\n            ...options,\n        }\n    );\n}\n", "/** @odoo-module */\n\nimport { HootDomError, getTag, isFirefox, isIterable } from \"../hoot_dom_utils\";\nimport {\n    getActiveElement,\n    getDocument,\n    getNextFocusableElement,\n    getNodeRect,\n    getNodeValue,\n    getPreviousFocusableElement,\n    getWindow,\n    isCheckable,\n    isEditable,\n    isEventTarget,\n    isNode,\n    isNodeFocusable,\n    isNodeVisible,\n    parseDimensions,\n    parsePosition,\n    queryAll,\n    queryFirst,\n    setDimensions,\n    toSelector,\n} from \"./dom\";\n\n/**\n * @typedef {Target | Promise<Target>} AsyncTarget\n *\n * @typedef {\"auto\" | \"blur\" | \"enter\" | \"tab\" | false} ConfirmAction\n *\n * @typedef {{\n *  cancel: (options?: EventOptions) => Promise<EventList>;\n *  drop: (to?: AsyncTarget, options?: PointerOptions) => Promise<EventList>;\n *  moveTo: (to?: AsyncTarget, options?: PointerOptions) => Promise<DragHelpers>;\n * }} DragHelpers\n *\n * @typedef {import(\"./dom\").Position} Position\n *\n * @typedef {import(\"./dom\").Dimensions} Dimensions\n *\n * @typedef {((ev: Event) => boolean) | EventType} EventListPredicate\n *\n * @typedef {{}} EventOptions generic event options\n *\n * @typedef {{\n *  clientX: number;\n *  clientY: number;\n *  pageX: number;\n *  pageY: number;\n *  screenX: number;\n *  screenY: number;\n * }} EventPosition\n *\n * @typedef {keyof HTMLElementEventMap | keyof WindowEventMap} EventType\n *\n * @typedef {EventOptions & {\n *  confirm?: ConfirmAction;\n *  composition?: boolean;\n *  instantly?: boolean;\n * }} FillOptions\n *\n * @typedef {string | number | MaybeIterable<File>} InputValue\n *\n * @typedef {EventOptions & KeyboardEventInit} KeyboardOptions\n *\n * @typedef {string | string[]} KeyStrokes\n *\n * @typedef {EventOptions & QueryOptions & {\n *  button?: number,\n *  position?: Side | `${Side}-${Side}` | Position;\n *  relative?: boolean;\n * }} PointerOptions\n *\n * @typedef {import(\"./dom\").QueryOptions} QueryOptions\n *\n * @typedef {EventOptions & {\n *  target: AsyncTarget;\n * }} SelectOptions\n *\n * @typedef {\"bottom\" | \"left\" | \"right\" | \"top\"} Side\n */\n\n/**\n * @template [T=EventInit]\n * @typedef {T & {\n *  target: EventTarget;\n *  type: EventType;\n * }} FullEventInit\n */\n\n/**\n * @template T\n * @typedef {T | Iterable<T>} MaybeIterable\n */\n\n/**\n * @template [T=Node]\n * @typedef {import(\"./dom\").Target<T>} Target\n */\n\n//-----------------------------------------------------------------------------\n// Global\n//-----------------------------------------------------------------------------\n\nconst {\n    AnimationEvent,\n    ClipboardEvent,\n    CompositionEvent,\n    console: { dir: $dir, groupCollapsed: $groupCollapsed, groupEnd: $groupEnd, log: $log },\n    DataTransfer,\n    document,\n    DragEvent,\n    ErrorEvent,\n    Event,\n    FocusEvent,\n    KeyboardEvent,\n    Math: { ceil: $ceil, max: $max, min: $min },\n    MouseEvent,\n    Number: { isInteger: $isInteger, isNaN: $isNaN, parseFloat: $parseFloat },\n    Object: { assign: $assign, values: $values },\n    PointerEvent,\n    PromiseRejectionEvent,\n    String,\n    SubmitEvent,\n    Touch,\n    TouchEvent,\n    TypeError,\n    WheelEvent,\n} = globalThis;\n/** @type {Document[\"createRange\"]} */\nconst $createRange = document.createRange.bind(document);\n/** @type {Document[\"hasFocus\"]} */\nconst $hasFocus = document.hasFocus.bind(document);\n\n//-----------------------------------------------------------------------------\n// Internal\n//-----------------------------------------------------------------------------\n\n/**\n * @param {EventTarget} target\n * @param {EventType} type\n */\nconst catchNextEvent = (target, type) =>\n    new Promise((resolve) => {\n        target.addEventListener(\n            type,\n            (event) => {\n                getCurrentEvents().push(event);\n                resolve(event);\n            },\n            { once: true }\n        );\n    });\n\n/**\n * @param {HTMLInputElement | HTMLTextAreaElement} target\n */\nconst deleteSelection = (target) => {\n    const { selectionStart, selectionEnd, value } = target;\n    return value.slice(0, selectionStart) + value.slice(selectionEnd);\n};\n\n/**\n *\n * @param {EventTarget} target\n * @param {EventType} eventType\n * @param {PointerEventInit} eventInit\n * @param {{\n *  mouse?: [EventType, MouseEventInit];\n *  touch?: [EventType, TouchEventInit];\n * }} additionalEvents\n */\nconst dispatchPointerEvent = async (target, eventType, eventInit, { mouse, touch }) => {\n    const pointerEvent = await dispatch(target, eventType, eventInit);\n    let prevented = isPrevented(pointerEvent);\n    if (hasTouch()) {\n        if (touch && runTime.pointerDownTarget) {\n            const [touchEventType, touchEventInit] = touch;\n            await dispatch(runTime.pointerDownTarget, touchEventType, touchEventInit || eventInit);\n        }\n    } else {\n        if (mouse && !prevented) {\n            const [mouseEventType, mouseEventInit] = mouse;\n            const mouseEvent = await dispatch(target, mouseEventType, mouseEventInit || eventInit);\n            prevented = isPrevented(mouseEvent);\n        }\n    }\n    return prevented;\n};\n\n/**\n * @param {Iterable<Event>} events\n * @param {EventType} eventType\n * @param {EventInit} eventInit\n */\nconst dispatchRelatedEvents = async (events, eventType, eventInit) => {\n    for (const event of events) {\n        if (!event.target || isPrevented(event)) {\n            break;\n        }\n        await dispatch(event.target, eventType, eventInit);\n    }\n};\n\n/**\n * @template T\n * @param {MaybeIterable<T>} value\n * @returns {T[]}\n */\nconst ensureArray = (value) => (isIterable(value) ? [...value] : [value]);\n\nconst getCurrentEvents = () => {\n    const eventType = currentEventTypes.at(-1);\n    if (!eventType) {\n        return [];\n    }\n    currentEvents[eventType] ||= [];\n    return currentEvents[eventType];\n};\n\nconst getDefaultRunTimeValue = () => ({\n    // Composition\n    isComposing: false,\n\n    // Drag & drop\n    canStartDrag: false,\n    isDragging: false,\n    lastDragOverCancelled: false,\n\n    // Pointer\n    clickCount: 0,\n    key: null,\n    pointerDownTarget: null,\n    pointerDownTimeout: 0,\n    pointerTarget: null,\n    /** @type {EventPosition | {}} */\n    position: {},\n    previousPointerDownTarget: null,\n    previousPointerTarget: null,\n    /** @type {EventPosition | {}} */\n    touchStartPosition: {},\n\n    // File\n    fileInput: null,\n});\n\nconst getDefaultSpecialKeysValue = () => ({\n    altKey: false,\n    ctrlKey: false,\n    metaKey: false,\n    shiftKey: false,\n});\n\n/**\n * Returns the list of nodes containing n2 (included) that do not contain n1.\n *\n * @param {Element} [el1]\n * @param {Element} [el2]\n */\nconst getDifferentParents = (el1, el2) => {\n    if (!el1 && !el2) {\n        // No given elements => no parents\n        return [];\n    } else if (!el1 && el2) {\n        // No first element => only parents of second element\n        [el1, el2] = [el2, el1];\n    }\n    const parents = [el2 || el1];\n    while (parents[0].parentElement) {\n        const parent = parents[0].parentElement;\n        if (el2 && parent.contains(el1)) {\n            break;\n        }\n        parents.unshift(parent);\n    }\n    return parents;\n};\n\n/**\n * @template {typeof Event} T\n * @param {EventType} eventType\n * @returns {[T, (attrs: FullEventInit) => EventInit]}\n */\nconst getEventConstructor = (eventType) => {\n    switch (eventType) {\n        // Mouse events\n        case \"auxclick\":\n        case \"contextmenu\":\n        case \"dblclick\":\n        case \"mousedown\":\n        case \"mouseup\":\n        case \"mousemove\":\n        case \"mouseover\":\n        case \"mouseout\":\n            return [MouseEvent, mapBubblingMouseEvent];\n        case \"mouseenter\":\n        case \"mouseleave\":\n            return [MouseEvent, mapNonBubblingMouseEvent];\n\n        // Pointer events\n        case \"click\":\n        case \"pointerdown\":\n        case \"pointerup\":\n        case \"pointermove\":\n        case \"pointerover\":\n        case \"pointerout\":\n            return [PointerEvent, mapBubblingPointerEvent];\n        case \"pointerenter\":\n        case \"pointerleave\":\n        case \"pointercancel\":\n            return [PointerEvent, mapNonBubblingPointerEvent];\n\n        // Focus events\n        case \"blur\":\n        case \"focus\":\n            return [FocusEvent, mapNonBubblingEvent];\n        case \"focusin\":\n        case \"focusout\":\n            return [FocusEvent, mapBubblingEvent];\n\n        // Clipboard events\n        case \"cut\":\n        case \"copy\":\n        case \"paste\":\n            return [ClipboardEvent, mapBubblingEvent];\n\n        // Keyboard events\n        case \"keydown\":\n        case \"keyup\":\n            return [KeyboardEvent, mapKeyboardEvent];\n\n        // Drag events\n        case \"drag\":\n        case \"dragend\":\n        case \"dragenter\":\n        case \"dragstart\":\n        case \"dragleave\":\n        case \"dragover\":\n        case \"drop\":\n            return [DragEvent, mapBubblingEvent];\n\n        // Input events\n        case \"beforeinput\":\n            return [InputEvent, mapCancelableInputEvent];\n        case \"input\":\n            return [InputEvent, mapInputEvent];\n\n        // Composition events\n        case \"compositionstart\":\n        case \"compositionend\":\n            return [CompositionEvent, mapBubblingEvent];\n\n        // Selection events\n        case \"select\":\n        case \"selectionchange\":\n            return [Event, mapBubblingEvent];\n\n        // Touch events\n        case \"touchstart\":\n        case \"touchend\":\n        case \"touchmove\":\n            return [TouchEvent, mapCancelableTouchEvent];\n        case \"touchcancel\":\n            return [TouchEvent, mapNonCancelableTouchEvent];\n\n        // Resize events\n        case \"resize\":\n            return [Event, mapNonBubblingEvent];\n\n        // Submit events\n        case \"submit\":\n            return [SubmitEvent, mapBubblingCancelableEvent];\n\n        // Wheel events\n        case \"wheel\":\n            return [WheelEvent, mapWheelEvent];\n\n        // Animation events\n        case \"animationcancel\":\n        case \"animationend\":\n        case \"animationiteration\":\n        case \"animationstart\": {\n            return [AnimationEvent, mapBubblingCancelableEvent];\n        }\n\n        // Error events\n        case \"error\":\n            return [ErrorEvent, mapNonBubblingEvent];\n        case \"unhandledrejection\":\n            return [PromiseRejectionEvent, mapNonBubblingCancelableEvent];\n\n        // Unload events (BeforeUnloadEvent cannot be constructed)\n        case \"beforeunload\":\n            return [Event, mapNonBubblingCancelableEvent];\n        case \"unload\":\n            return [Event, mapNonBubblingEvent];\n\n        // Default: base Event constructor\n        default:\n            return [Event, mapBubblingEvent];\n    }\n};\n\n/**\n * @param {Node} [a]\n * @param {Node} [b]\n */\nconst getFirstCommonParent = (a, b) => {\n    if (!a || !b || a.ownerDocument !== b.ownerDocument) {\n        return null;\n    }\n\n    const range = document.createRange();\n    range.setStart(a, 0);\n    range.setEnd(b, 0);\n\n    if (range.collapsed) {\n        // Re-arranges range if the first node comes after the second\n        range.setStart(b, 0);\n        range.setEnd(a, 0);\n    }\n\n    return range.commonAncestorContainer;\n};\n\n/**\n * @param {HTMLElement} element\n * @param {PointerOptions} [options]\n */\nconst getPosition = (element, options) => {\n    const { position, relative } = options || {};\n    const isString = typeof position === \"string\";\n    const [posX, posY] = parsePosition(position);\n\n    if (!isString && !relative && !$isNaN(posX) && !$isNaN(posY)) {\n        // Absolute position\n        return toEventPosition(posX, posY, position);\n    }\n\n    const { x, y, width, height } = getNodeRect(element);\n    let clientX = x;\n    let clientY = y;\n\n    if (isString) {\n        const positions = position.split(\"-\");\n\n        // X position\n        if (positions.includes(\"left\")) {\n            clientX -= 1;\n        } else if (positions.includes(\"right\")) {\n            clientX += $ceil(width) + 1;\n        } else {\n            clientX += width / 2;\n        }\n\n        // Y position\n        if (positions.includes(\"top\")) {\n            clientY -= 1;\n        } else if (positions.includes(\"bottom\")) {\n            clientY += $ceil(height) + 1;\n        } else {\n            clientY += height / 2;\n        }\n    } else {\n        // X position\n        if ($isNaN(posX)) {\n            clientX += width / 2;\n        } else {\n            if (relative) {\n                clientX += posX || 0;\n            } else {\n                clientX = posX || 0;\n            }\n        }\n\n        // Y position\n        if ($isNaN(posY)) {\n            clientY += height / 2;\n        } else {\n            if (relative) {\n                clientY += posY || 0;\n            } else {\n                clientY = posY || 0;\n            }\n        }\n    }\n\n    return toEventPosition(clientX, clientY, position);\n};\n\n/**\n * @param {Node} target\n */\nconst getStringSelection = (target) =>\n    $isInteger(target.selectionStart) &&\n    $isInteger(target.selectionEnd) &&\n    [target.selectionStart, target.selectionEnd].join(\",\");\n\n/**\n * @param {Node} node\n * @param  {...string} tagNames\n */\nconst hasTagName = (node, ...tagNames) => tagNames.includes(getTag(node));\n\nconst hasTouch = () =>\n    globalThis.ontouchstart !== undefined || globalThis.matchMedia(\"(pointer:coarse)\").matches;\n\n/**\n * @param {EventTarget | EventPosition} target\n * @param {PointerOptions} [options]\n */\nconst isDifferentPosition = (target, options) => {\n    const previous = runTime.position;\n    const next = isNode(target) ? getPosition(target, options) : target;\n    for (const key in next) {\n        if (previous[key] !== next[key]) {\n            return true;\n        }\n    }\n    return false;\n};\n\n/**\n * @param {unknown} value\n */\nconst isNil = (value) => value === null || value === undefined;\n\n/**\n * @param {Event} event\n */\nconst isPrevented = (event) => event && event.defaultPrevented;\n\n/**\n * @param {KeyStrokes} keyStrokes\n * @param {KeyboardEventInit} [options]\n * @returns {KeyboardEventInit}\n */\nconst parseKeyStrokes = (keyStrokes, options) =>\n    (isIterable(keyStrokes) ? [...keyStrokes] : [keyStrokes]).map((key) => {\n        const lower = key.toLowerCase();\n        return {\n            ...options,\n            key: lower.length === 1 ? key : KEY_ALIASES[lower] || key,\n        };\n    });\n\n/**\n * @param {Event} ev\n */\nconst registerFileInput = ({ target }) => {\n    if (getTag(target) === \"input\" && target.type === \"file\") {\n        runTime.fileInput = target;\n    } else {\n        runTime.fileInput = null;\n    }\n};\n\n/**\n * @param {EventTarget} target\n * @param {string} initialValue\n * @param {ConfirmAction} confirmAction\n */\nconst registerForChange = async (target, initialValue, confirmAction) => {\n    const triggerChange = () => {\n        removeChangeTargetListeners();\n\n        if (target.value !== initialValue) {\n            afterNextDispatch = () => dispatch(target, \"change\");\n        }\n    };\n\n    confirmAction &&= confirmAction.toLowerCase();\n    if (confirmAction === \"auto\") {\n        confirmAction = getTag(target) === \"input\" ? \"enter\" : \"blur\";\n    }\n    if (confirmAction === \"enter\") {\n        if (getTag(target) === \"input\") {\n            changeTargetListeners.push(\n                on(\n                    target,\n                    \"keydown\",\n                    (ev) => !isPrevented(ev) && ev.key === \"Enter\" && triggerChange()\n                )\n            );\n        } else {\n            throw new HootDomError(`\"enter\" confirm action is only supported on <input/> elements`);\n        }\n    }\n\n    changeTargetListeners.push(\n        on(target, \"blur\", triggerChange),\n        on(target, \"change\", removeChangeTargetListeners)\n    );\n\n    switch (confirmAction) {\n        case \"blur\": {\n            await _click(getDocument(target).body, {\n                position: { x: 0, y: 0 },\n            });\n            break;\n        }\n        case \"enter\": {\n            await _press(target, { key: \"Enter\" });\n            break;\n        }\n        case \"tab\": {\n            await _press(target, { key: \"Tab\" });\n            break;\n        }\n    }\n};\n\n/**\n * @param {KeyboardEventInit} eventInit\n * @param {boolean} toggle\n */\nconst registerSpecialKey = (eventInit, toggle) => {\n    switch (eventInit.key) {\n        case \"Alt\":\n            specialKeys.altKey = toggle;\n            break;\n        case \"Control\":\n            specialKeys.ctrlKey = toggle;\n            break;\n        case \"Meta\":\n            specialKeys.metaKey = toggle;\n            break;\n        case \"Shift\":\n            specialKeys.shiftKey = toggle;\n            break;\n    }\n};\n\nconst removeChangeTargetListeners = () => {\n    while (changeTargetListeners.length) {\n        changeTargetListeners.pop()();\n    }\n};\n\n/**\n * @param {HTMLElement | null} target\n */\nconst setPointerDownTarget = (target) => {\n    if (runTime.pointerDownTarget) {\n        runTime.previousPointerDownTarget = runTime.pointerDownTarget;\n    }\n    runTime.pointerDownTarget = target;\n    runTime.canStartDrag = false;\n};\n\n/**\n * @param {HTMLElement | null} target\n * @param {PointerOptions} [options]\n */\nconst setPointerTarget = async (target, options) => {\n    runTime.previousPointerTarget = runTime.pointerTarget;\n    runTime.pointerTarget = target;\n\n    if (runTime.pointerTarget !== runTime.previousPointerTarget && runTime.canStartDrag) {\n        /**\n         * Special action: drag start\n         *  On: unprevented 'pointerdown' on a draggable element (DESKTOP ONLY)\n         *  Do: triggers a 'dragstart' event\n         */\n        const dragStartEvent = await dispatch(runTime.previousPointerTarget, \"dragstart\");\n\n        runTime.isDragging = !isPrevented(dragStartEvent);\n        runTime.canStartDrag = false;\n    }\n\n    runTime.position = target && getPosition(target, options);\n};\n\n/**\n * @param {string} type\n */\nconst setupEvents = (type) => {\n    currentEventTypes.push(type);\n\n    return async () => {\n        const events = new EventList(getCurrentEvents());\n        const currentType = currentEventTypes.pop();\n        delete currentEvents[currentType];\n        if (!allowLogs) {\n            return events;\n        }\n        const groupName = [`${type}: dispatched`, events.length, `events`];\n        $groupCollapsed(...groupName);\n        for (const event of events) {\n            /** @type {(keyof typeof LOG_COLORS)[]} */\n            const colors = [\"blue\"];\n\n            const typeList = [event.type];\n            if (event.key) {\n                typeList.push(event.key);\n            } else if (event.button) {\n                typeList.push(event.button);\n            }\n            [...Array(typeList.length)].forEach(() => colors.push(\"orange\"));\n\n            const typeString = typeList.map((t) => `%c\"${t}\"%c`).join(\", \");\n            let message = `%c${event.constructor.name}%c<${typeString}>`;\n            if (event.__bubbleCount) {\n                message += ` (${event.__bubbleCount})`;\n            }\n            const target = event.__originalTarget || event.target;\n            if (isNode(target)) {\n                const targetParts = toSelector(target, { object: true });\n                colors.push(\"blue\");\n                if (targetParts.id) {\n                    colors.push(\"orange\");\n                }\n                if (targetParts.class) {\n                    colors.push(\"lightBlue\");\n                }\n                const targetString = $values(targetParts)\n                    .map((part) => `%c${part}%c`)\n                    .join(\"\");\n                message += ` @${targetString}`;\n            }\n            const messageColors = colors.flatMap((color) => [\n                `color: ${LOG_COLORS[color]}; font-weight: normal`,\n                `color: ${LOG_COLORS.reset}`,\n            ]);\n\n            $groupCollapsed(message, ...messageColors);\n            $dir(event);\n            $log(target);\n            $groupEnd();\n        }\n        $groupEnd();\n\n        return events;\n    };\n};\n\n/**\n * @param {number} clientX\n * @param {number} clientY\n * @param {Partial<EventPosition>} [position]\n */\nconst toEventPosition = (clientX, clientY, position) => {\n    clientX ||= 0;\n    clientY ||= 0;\n    return {\n        clientX,\n        clientY,\n        pageX: position?.pageX ?? clientX,\n        pageY: position?.pageY ?? clientY,\n        screenX: position?.screenX ?? clientX,\n        screenY: position?.screenY ?? clientY,\n    };\n};\n\n/**\n * @param {EventTarget} target\n * @param {PointerEventInit} eventInit\n */\nconst triggerClick = async (target, pointerInit) => {\n    if (target.disabled) {\n        return;\n    }\n    const clickEvent = await dispatch(target, \"click\", pointerInit);\n    if (isPrevented(clickEvent)) {\n        return;\n    }\n    if (isFirefox()) {\n        // Thanks Firefox\n        switch (getTag(target)) {\n            case \"label\": {\n                /**\n                 * @firefox\n                 * Special action: label 'Click'\n                 *  On: unprevented 'click' on a <label/>\n                 *  Do: triggers a 'click' event on the first <input/> descendant\n                 */\n                target = target.control;\n                if (target) {\n                    await triggerClick(target, pointerInit);\n                }\n                break;\n            }\n            case \"option\": {\n                /**\n                 * @firefox\n                 * Special action: option 'Click'\n                 *  On: unprevented 'click' on an <option/>\n                 *  Do: triggers a 'change' event on the parent <select/>\n                 */\n                const parent = target.parentElement;\n                if (parent && getTag(parent) === \"select\") {\n                    await dispatch(parent, \"change\");\n                }\n                break;\n            }\n        }\n    }\n};\n\n/**\n * @param {EventTarget} target\n * @param {DragEventInit} eventInit\n */\nconst triggerDrag = async (target, eventInit) => {\n    await dispatch(target, \"drag\", eventInit);\n    // Only \"dragover\" being prevented is taken into account for \"drop\" events\n    const dragOverEvent = await dispatch(target, \"dragover\", eventInit);\n    runTime.lastDragOverCancelled = isPrevented(dragOverEvent);\n};\n\n/**\n * @param {EventTarget} target\n */\nconst triggerFocus = async (target) => {\n    const previous = getActiveElement(target);\n    if (previous === target) {\n        return;\n    }\n    if (previous !== target.ownerDocument.body) {\n        if ($hasFocus() && isNodeVisible(previous)) {\n            catchNextEvent(previous, \"focusout\");\n        }\n        // If document is focused, this will trigger a trusted \"blur\" event\n        previous.blur();\n        if (!$hasFocus()) {\n            // When document is not focused: manually trigger a \"blur\" event\n            const eventInit = { relatedTarget: target };\n            await dispatch(previous, \"blur\", eventInit);\n            await dispatch(previous, \"focusout\", eventInit);\n        }\n    }\n    if (isNodeFocusable(target)) {\n        const previousSelection = getStringSelection(target);\n\n        // If document is focused, this will trigger a trusted \"focus\" event\n        if ($hasFocus() && isNodeVisible(target)) {\n            catchNextEvent(target, \"focusin\");\n        }\n        target.focus();\n        if (!$hasFocus()) {\n            // When document is not focused: manually trigger a \"focus\" event\n            const eventInit = { relatedTarget: previous };\n            await dispatch(target, \"focus\", eventInit);\n            await dispatch(target, \"focusin\", eventInit);\n        }\n\n        if (previousSelection && previousSelection === getStringSelection(target)) {\n            target.selectionStart = target.selectionEnd = target.value.length;\n        }\n    }\n};\n\n/**\n * @param {EventTarget} target\n * @param {FillOptions} options\n */\nconst _clear = async (target, options) => {\n    // Inputs and text areas\n    const initialValue = target.value;\n\n    // Simulates 2 key presses:\n    // - Control + A: selects all the text\n    // - Backspace: deletes the text\n    fullClear = true;\n    await _press(target, { ctrlKey: true, key: \"a\" });\n    await _press(target, { key: \"Backspace\" });\n    fullClear = false;\n\n    await registerForChange(target, initialValue, options?.confirm);\n};\n\n/**\n * @param {EventTarget} target\n * @param {PointerOptions} [option]\n */\nconst _click = async (target, options) => {\n    await _pointerDown(target, options);\n    await _pointerUp(target, options);\n};\n\n/**\n * @param {EventTarget} target\n * @param {InputValue} value\n * @param {FillOptions} [options]\n */\nconst _fill = async (target, value, options) => {\n    const initialValue = target.value;\n\n    if (getTag(target) === \"input\") {\n        switch (target.type) {\n            case \"color\": {\n                target.value = String(value);\n                await dispatch(target, \"input\");\n                await dispatch(target, \"change\");\n                return;\n            }\n            case \"file\": {\n                const dataTransfer = new DataTransfer();\n                const files = ensureArray(value);\n                if (files.length > 1 && !target.multiple) {\n                    throw new HootDomError(`input[type=\"file\"] does not support multiple files`);\n                }\n                for (const file of files) {\n                    if (!(file instanceof File)) {\n                        throw new TypeError(`file input only accept 'File' objects`);\n                    }\n                    dataTransfer.items.add(file);\n                }\n                target.files = dataTransfer.files;\n\n                await dispatch(target, \"change\");\n                return;\n            }\n            case \"range\": {\n                const numberValue = $parseFloat(value);\n                if ($isNaN(numberValue)) {\n                    throw new TypeError(`input[type=\"range\"] only accept 'number' values`);\n                }\n\n                target.value = String(numberValue);\n                await dispatch(target, \"input\");\n                await dispatch(target, \"change\");\n                return;\n            }\n        }\n    }\n\n    if (options?.instantly) {\n        // Simulates filling the clipboard with the value (can be from external source)\n        globalThis.navigator.clipboard.writeText(value).catch();\n        await _press(target, { ctrlKey: true, key: \"v\" });\n    } else {\n        if (options?.composition) {\n            runTime.isComposing = true;\n            // Simulates the start of a composition\n            await dispatch(target, \"compositionstart\");\n        }\n        for (const char of String(value)) {\n            const key = char.toLowerCase();\n            await _press(target, { key, shiftKey: key !== char });\n        }\n        if (options?.composition) {\n            runTime.isComposing = false;\n            // Simulates the end of a composition\n            await dispatch(target, \"compositionend\");\n        }\n    }\n\n    await registerForChange(target, initialValue, options?.confirm);\n};\n\n/**\n * @param {EventTarget} target\n * @param {PointerOptions} options\n */\nconst _hover = async (target, options) => {\n    const isDifferentTarget = target !== runTime.pointerTarget;\n    const previousPosition = runTime.position;\n\n    await setPointerTarget(target, options);\n\n    const { previousPointerTarget: previous, pointerTarget: current } = runTime;\n    if (isDifferentTarget && previous && (!current || !previous.contains(current))) {\n        // Leaves previous target\n        const leaveEventInit = {\n            ...previousPosition,\n            relatedTarget: current,\n        };\n\n        if (runTime.isDragging) {\n            // If dragging, only drag events are triggered\n            await triggerDrag(previous, leaveEventInit);\n            await dispatch(previous, \"dragleave\", leaveEventInit);\n        } else {\n            // Regular case: pointer events are triggered\n            await dispatchPointerEvent(previous, \"pointermove\", leaveEventInit, {\n                mouse: [\"mousemove\"],\n                touch: [\"touchmove\"],\n            });\n            await dispatchPointerEvent(previous, \"pointerout\", leaveEventInit, {\n                mouse: [\"mouseout\"],\n            });\n            const leaveEvents = await Promise.all(\n                getDifferentParents(current, previous).map((element) =>\n                    dispatch(element, \"pointerleave\", leaveEventInit)\n                )\n            );\n            if (!hasTouch()) {\n                await dispatchRelatedEvents(leaveEvents, \"mouseleave\", leaveEventInit);\n            }\n        }\n    }\n\n    if (current) {\n        const enterEventInit = {\n            ...runTime.position,\n            relatedTarget: previous,\n        };\n        if (runTime.isDragging) {\n            // If dragging, only drag events are triggered\n            if (isDifferentTarget) {\n                await dispatch(target, \"dragenter\", enterEventInit);\n            }\n            await triggerDrag(target, enterEventInit);\n        } else {\n            // Regular case: pointer events are triggered\n            if (isDifferentTarget) {\n                await dispatchPointerEvent(target, \"pointerover\", enterEventInit, {\n                    mouse: [\"mouseover\"],\n                });\n                const enterEvents = await Promise.all(\n                    getDifferentParents(previous, current).map((element) =>\n                        dispatch(element, \"pointerenter\", enterEventInit)\n                    )\n                );\n                if (!hasTouch()) {\n                    await dispatchRelatedEvents(enterEvents, \"mouseenter\", enterEventInit);\n                }\n            }\n            await dispatchPointerEvent(target, \"pointermove\", enterEventInit, {\n                mouse: [\"mousemove\"],\n                touch: [\"touchmove\"],\n            });\n        }\n    }\n};\n\n/**\n * @param {EventTarget} target\n * @param {PointerOptions} [options]\n */\nconst _implicitHover = async (target, options) => {\n    if (runTime.pointerTarget !== target || isDifferentPosition(target, options)) {\n        await _hover(target, options);\n    }\n};\n\n/**\n * @param {EventTarget} target\n * @param {KeyboardEventInit} eventInit\n */\nconst _keyDown = async (target, eventInit) => {\n    registerSpecialKey(eventInit, true);\n\n    const repeat =\n        typeof eventInit.repeat === \"boolean\" ? eventInit.repeat : runTime.key === eventInit.key;\n    runTime.key = eventInit.key;\n    const keyDownEvent = await dispatch(target, \"keydown\", { ...eventInit, repeat });\n\n    if (isPrevented(keyDownEvent)) {\n        return;\n    }\n\n    /**\n     * @param {string} toInsert\n     * @param {string} type\n     */\n    const insertValue = (toInsert, type) => {\n        const { selectionStart, selectionEnd, value } = target;\n        inputData = toInsert;\n        inputType = type;\n        if (isNil(selectionStart) && isNil(selectionEnd)) {\n            nextValue += toInsert;\n        } else {\n            nextValue = value.slice(0, selectionStart) + toInsert + value.slice(selectionEnd);\n            if (selectionStart === selectionEnd) {\n                nextSelectionStart = nextSelectionEnd = selectionStart + 1;\n            }\n        }\n    };\n\n    const { ctrlKey, key, shiftKey } = keyDownEvent;\n    let inputData = null;\n    let inputType = null;\n    let nextSelectionEnd = null;\n    let nextSelectionStart = null;\n    let nextValue = target.value;\n\n    if (isEditable(target)) {\n        switch (key) {\n            case \"ArrowDown\":\n            case \"ArrowLeft\":\n            case \"ArrowUp\":\n            case \"ArrowRight\": {\n                const { selectionStart, selectionEnd, value } = target;\n                if (isNil(selectionStart) || isNil(selectionEnd)) {\n                    break;\n                }\n                const start = key === \"ArrowLeft\" || key === \"ArrowUp\";\n                let selectionTarget;\n                if (ctrlKey) {\n                    // Move to the start/end of the line\n                    selectionTarget = start ? 0 : value.length;\n                } else {\n                    // Move the cursor left or right\n                    selectionTarget = start ? selectionStart - 1 : selectionEnd + 1;\n                }\n                target.selectionStart = target.selectionEnd = $max(\n                    $min(selectionTarget, value.length),\n                    0\n                );\n                break;\n            }\n            case \"Backspace\": {\n                const { selectionStart, selectionEnd, value } = target;\n                if (fullClear) {\n                    // Remove all characters\n                    nextValue = \"\";\n                } else if (isNil(selectionStart) || isNil(selectionEnd)) {\n                    // Remove last character\n                    nextValue = value.slice(0, -1);\n                } else if (selectionStart === selectionEnd) {\n                    // Remove previous character from target value\n                    nextValue = value.slice(0, selectionStart - 1) + value.slice(selectionEnd);\n                } else {\n                    // Remove current selection from target value\n                    nextValue = deleteSelection(target);\n                }\n                inputType = \"deleteContentBackward\";\n                break;\n            }\n            case \"Delete\": {\n                const { selectionStart, selectionEnd, value } = target;\n                if (fullClear) {\n                    // Remove all characters\n                    nextValue = \"\";\n                } else if (isNil(selectionStart) || isNil(selectionEnd)) {\n                    // Remove first character\n                    nextValue = value.slice(1);\n                } else if (selectionStart === selectionEnd) {\n                    // Remove next character from target value\n                    nextValue = value.slice(0, selectionStart) + value.slice(selectionEnd + 1);\n                } else {\n                    // Remove current selection from target value\n                    nextValue = deleteSelection(target);\n                }\n                inputType = \"deleteContentForward\";\n                break;\n            }\n            case \"Enter\": {\n                if (target.tagName === \"TEXTAREA\") {\n                    // Insert new line\n                    insertValue(\"\\n\", \"insertLineBreak\");\n                }\n                break;\n            }\n            default: {\n                if (key.length === 1 && !ctrlKey) {\n                    // Character coming from the keystroke\n                    // ! TODO: Doesn't work with non-roman locales\n                    insertValue(\n                        shiftKey ? key.toUpperCase() : key.toLowerCase(),\n                        runTime.isComposing ? \"insertCompositionText\" : \"insertText\"\n                    );\n                }\n            }\n        }\n    }\n\n    switch (key) {\n        case \"a\": {\n            if (ctrlKey) {\n                // Select all\n                if (isEditable(target)) {\n                    await dispatch(target, \"select\");\n                    if (!isNil(target.selectionStart) && !isNil(target.selectionEnd)) {\n                        target.selectionStart = 0;\n                        target.selectionEnd = target.value.length;\n                    }\n                } else {\n                    const selection = globalThis.getSelection();\n                    const range = $createRange();\n                    range.selectNodeContents(target);\n                    selection.removeAllRanges();\n                    selection.addRange(range);\n                }\n            }\n            break;\n        }\n        /**\n         * Special action: copy\n         *  On: unprevented 'Control + c' keydown\n         *  Do: copy current selection to clipboard\n         */\n        case \"c\": {\n            if (ctrlKey) {\n                // Get selection from window\n                const text = globalThis.getSelection().toString();\n                globalThis.navigator.clipboard.writeText(text).catch();\n\n                await dispatch(target, \"copy\", {\n                    clipboardData: eventInit.dataTransfer || new DataTransfer(),\n                });\n            }\n            break;\n        }\n        case \"Enter\": {\n            const tag = getTag(target);\n            const parentForm = target.closest(\"form\");\n            if (parentForm && target.type !== \"button\") {\n                /**\n                 * Special action: <form> 'Enter'\n                 *  On: unprevented 'Enter' keydown on any element that\n                 *      is not a <button type=\"button\"/> in a form element\n                 *  Do: triggers a 'submit' event on the form\n                 */\n                await dispatch(parentForm, \"submit\");\n            } else if (\n                !keyDownEvent.repeat &&\n                (tag === \"a\" || tag === \"button\" || (tag === \"input\" && target.type === \"button\"))\n            ) {\n                /**\n                 * Special action: <a>, <button> or <input type=\"button\"> 'Enter'\n                 *  On: unprevented and unrepeated 'Enter' keydown on mentioned elements\n                 *  Do: triggers a 'click' event on the element\n                 */\n                await dispatch(target, \"click\", { button: 0 });\n            }\n            break;\n        }\n        case \"Escape\": {\n            runTime.isDragging = false;\n            break;\n        }\n        /**\n         * Special action: shift focus\n         *  On: unprevented 'Tab' keydown\n         *  Do: focus next (or previous with 'Shift') focusable element\n         */\n        case \"Tab\": {\n            const next = shiftKey\n                ? getPreviousFocusableElement({ tabbable: true })\n                : getNextFocusableElement({ tabbable: true });\n            if (next) {\n                await triggerFocus(next);\n            }\n            break;\n        }\n        /**\n         * Special action: paste\n         *  On: unprevented 'Control + v' keydown on editable element\n         *  Do: paste current clipboard content to current element\n         */\n        case \"v\": {\n            if (ctrlKey && isEditable(target)) {\n                // Set target value (if possible)\n                try {\n                    nextValue = await globalThis.navigator.clipboard.readText();\n                } catch (err) {}\n                inputType = \"insertFromPaste\";\n\n                await dispatch(target, \"paste\", {\n                    clipboardData: eventInit.dataTransfer || new DataTransfer(),\n                });\n            }\n            break;\n        }\n        /**\n         * Special action: cut\n         *  On: unprevented 'Control + x' keydown on editable element\n         *  Do: cut current selection to clipboard and remove selection\n         */\n        case \"x\": {\n            if (ctrlKey && isEditable(target)) {\n                // Get selection from window\n                const text = globalThis.getSelection().toString();\n                globalThis.navigator.clipboard.writeText(text).catch();\n\n                nextValue = deleteSelection(target);\n                inputType = \"deleteByCut\";\n\n                await dispatch(target, \"cut\", {\n                    clipboardData: eventInit.dataTransfer || new DataTransfer(),\n                });\n            }\n            break;\n        }\n    }\n\n    if (target.value !== nextValue) {\n        target.value = nextValue;\n        if (!isNil(nextSelectionStart)) {\n            target.selectionStart = nextSelectionStart;\n        }\n        if (!isNil(nextSelectionEnd)) {\n            target.selectionEnd = nextSelectionEnd;\n        }\n        const inputEventInit = {\n            data: inputData,\n            inputType,\n        };\n        const beforeInputEvent = await dispatch(target, \"beforeinput\", inputEventInit);\n        if (!isPrevented(beforeInputEvent)) {\n            await dispatch(target, \"input\", inputEventInit);\n        }\n    }\n};\n\n/**\n * @param {EventTarget} target\n * @param {KeyboardEventInit} eventInit\n */\nconst _keyUp = async (target, eventInit) => {\n    await dispatch(target, \"keyup\", eventInit);\n\n    runTime.key = null;\n    registerSpecialKey(eventInit, false);\n\n    if (eventInit.key === \" \" && getTag(target) === \"input\" && target.type === \"checkbox\") {\n        /**\n         * Special action: input[type=checkbox] 'Space'\n         *  On: unprevented ' ' keydown on an <input type=\"checkbox\"/>\n         *  Do: triggers a 'click' event on the input\n         */\n        await triggerClick(target, { button: 0 });\n    }\n};\n\n/**\n * @param {EventTarget} target\n * @param {PointerOptions} options\n */\nconst _pointerDown = async (target, options) => {\n    setPointerDownTarget(target);\n\n    const pointerDownTarget = runTime.pointerDownTarget;\n    const eventInit = {\n        ...runTime.position,\n        button: options?.button || 0,\n    };\n\n    if (pointerDownTarget !== runTime.previousPointerDownTarget) {\n        runTime.clickCount = 0;\n    }\n\n    runTime.touchStartPosition = { ...runTime.position };\n    const prevented = await dispatchPointerEvent(pointerDownTarget, \"pointerdown\", eventInit, {\n        mouse: !pointerDownTarget.disabled && [\n            \"mousedown\",\n            { ...eventInit, detail: runTime.clickCount + 1 },\n        ],\n        touch: [\"touchstart\"],\n    });\n\n    if (prevented) {\n        return;\n    }\n\n    // Focus the element (if focusable)\n    await triggerFocus(target);\n\n    if (eventInit.button === 0 && !hasTouch() && pointerDownTarget.draggable) {\n        runTime.canStartDrag = true;\n    } else if (eventInit.button === 2) {\n        /**\n         * Special action: context menu\n         *  On: unprevented 'pointerdown' with right click and its related\n         *      event on an element\n         *  Do: triggers a 'contextmenu' event\n         */\n        await dispatch(target, \"contextmenu\", eventInit);\n    }\n};\n\n/**\n * @param {EventTarget} target\n * @param {PointerOptions} options\n */\nconst _pointerUp = async (target, options) => {\n    const pointerDownTarget = runTime.pointerDownTarget;\n    const eventInit = {\n        ...runTime.position,\n        button: options?.button || 0,\n    };\n\n    if (runTime.isDragging) {\n        // If dragging, only drag events are triggered\n        runTime.isDragging = false;\n        if (runTime.lastDragOverCancelled) {\n            /**\n             * Special action: drop\n             * - On: prevented 'dragover'\n             * - Do: triggers a 'drop' event on the target\n             */\n            await dispatch(target, \"drop\", eventInit);\n        }\n\n        await dispatch(target, \"dragend\", eventInit);\n        return;\n    }\n\n    const mouseEventInit = {\n        ...eventInit,\n        detail: runTime.clickCount + 1,\n    };\n    await dispatchPointerEvent(target, \"pointerup\", eventInit, {\n        mouse: !target.disabled && [\"mouseup\", mouseEventInit],\n        touch: [\"touchend\"],\n    });\n\n    const touchStartPosition = runTime.touchStartPosition;\n    runTime.touchStartPosition = {};\n\n    if (hasTouch() && isDifferentPosition(touchStartPosition)) {\n        // No further event is trigger: there was a swiping motion since the \"touchstart\"\n        // event.\n        return;\n    }\n\n    let actualTarget;\n    if (hasTouch()) {\n        actualTarget = pointerDownTarget === target && target;\n    } else {\n        actualTarget = getFirstCommonParent(target, pointerDownTarget);\n    }\n    if (actualTarget) {\n        await triggerClick(actualTarget, mouseEventInit);\n        runTime.clickCount++;\n        if (!hasTouch() && runTime.clickCount % 2 === 0) {\n            await dispatch(actualTarget, \"dblclick\", mouseEventInit);\n        }\n    }\n\n    setPointerDownTarget(null);\n    if (runTime.pointerDownTimeout) {\n        globalThis.clearTimeout(runTime.pointerDownTimeout);\n    }\n    runTime.pointerDownTimeout = globalThis.setTimeout(() => {\n        // Use `globalThis.setTimeout` to potentially make use of the mock timeouts\n        // since the events run in the same temporal context as the tests\n        runTime.clickCount = 0;\n        runTime.pointerDownTimeout = 0;\n    }, DOUBLE_CLICK_DELAY);\n};\n\n/**\n * @param {EventTarget} target\n * @param {KeyboardEventInit} eventInit\n */\nconst _press = async (target, eventInit) => {\n    await _keyDown(target, eventInit);\n    await _keyUp(target, eventInit);\n};\n\n/**\n * @param {EventTarget} target\n * @param {string | number | (string | number)[]} value\n */\nconst _select = async (target, value) => {\n    const values = ensureArray(value).map(String);\n    let found = false;\n    for (const option of target.options) {\n        option.selected = values.includes(option.value);\n        found ||= option.selected;\n    }\n    if (!value) {\n        target.selectedIndex = -1;\n    } else if (!found) {\n        throw new HootDomError(\n            `error when calling \\`select()\\`: no option found with value \"${values.join(\", \")}\"`\n        );\n    }\n    await dispatch(target, \"change\");\n};\n\nconst DEPRECATED_EVENT_PROPERTIES = {\n    keyCode: \"key\",\n    which: \"key\",\n};\nconst DEPRECATED_EVENTS = {\n    keypress: \"keydown\",\n    mousewheel: \"wheel\",\n};\nconst DOUBLE_CLICK_DELAY = 500;\nconst KEY_ALIASES = {\n    // case insensitive aliases\n    alt: \"Alt\",\n    arrowdown: \"ArrowDown\",\n    arrowleft: \"ArrowLeft\",\n    arrowright: \"ArrowRight\",\n    arrowup: \"ArrowUp\",\n    backspace: \"Backspace\",\n    control: \"Control\",\n    delete: \"Delete\",\n    enter: \"Enter\",\n    escape: \"Escape\",\n    meta: \"Meta\",\n    shift: \"Shift\",\n    tab: \"Tab\",\n\n    // Other aliases\n    caps: \"Shift\",\n    cmd: \"Meta\",\n    command: \"Meta\",\n    ctrl: \"Control\",\n    del: \"Delete\",\n    down: \"ArrowDown\",\n    esc: \"Escape\",\n    left: \"ArrowLeft\",\n    right: \"ArrowRight\",\n    space: \" \",\n    up: \"ArrowUp\",\n    win: \"Meta\",\n};\nconst LOG_COLORS = {\n    blue: \"#5db0d7\",\n    orange: \"#f29364\",\n    lightBlue: \"#9bbbdc\",\n    reset: \"inherit\",\n};\n\n/** @type {Record<string, Event[]>} */\nconst currentEvents = {};\n/** @type {string[]} */\nconst currentEventTypes = [];\n/** @type {(() => Promise<void>) | null} */\nlet afterNextDispatch = null;\nlet allowLogs = false;\nlet fullClear = false;\n\n// Keyboard global variables\nconst changeTargetListeners = [];\nconst specialKeys = getDefaultSpecialKeysValue();\n\n// Other global variables\nconst runTime = getDefaultRunTimeValue();\n\n//-----------------------------------------------------------------------------\n// Event init attributes mappers\n//-----------------------------------------------------------------------------\n\n// Generic mappers\n// ---------------\n\n/**\n * - bubbles\n * - can be canceled\n * @param {FullEventInit} eventInit\n */\nconst mapBubblingCancelableEvent = (eventInit) => ({\n    ...mapBubblingEvent(eventInit),\n    cancelable: true,\n});\n\n/**\n * - bubbles\n * - cannot be canceled\n * @param {FullEventInit} eventInit\n */\nconst mapBubblingEvent = (eventInit) => ({\n    composed: true,\n    ...eventInit,\n    bubbles: true,\n});\n\n/**\n * - does not bubble\n * - can be canceled\n * @param {FullEventInit} eventInit\n */\nconst mapNonBubblingCancelableEvent = (eventInit) => ({\n    ...mapNonBubblingEvent(eventInit),\n    cancelable: true,\n});\n\n/**\n * - does not bubble\n * - cannot be canceled\n * @param {FullEventInit} eventInit\n */\nconst mapNonBubblingEvent = (eventInit) => ({\n    composed: true,\n    ...eventInit,\n});\n\n// Pointer, mouse & wheel event mappers\n// ------------------------------------\n\n/**\n * @param {FullEventInit<MouseEventInit>} eventInit\n */\nconst mapBubblingMouseEvent = (eventInit) => ({\n    clientX: eventInit.clientX ?? eventInit.pageX ?? eventInit.screenX ?? 0,\n    clientY: eventInit.clientY ?? eventInit.pageY ?? eventInit.screenY ?? 0,\n    view: getWindow(),\n    ...specialKeys,\n    ...mapBubblingCancelableEvent(eventInit),\n});\n\n/**\n * @param {FullEventInit<MouseEventInit>} eventInit\n */\nconst mapNonBubblingMouseEvent = (eventInit) => ({\n    ...mapBubblingMouseEvent(eventInit),\n    bubbles: false,\n    cancelable: false,\n});\n\n/**\n * @param {FullEventInit<PointerEventInit>} eventInit\n */\nconst mapBubblingPointerEvent = (eventInit) => ({\n    pointerId: 1,\n    pointerType: hasTouch() ? \"touch\" : \"mouse\",\n    ...mapBubblingMouseEvent(eventInit),\n});\n\n/**\n * @param {FullEventInit<PointerEventInit>} eventInit\n */\nconst mapNonBubblingPointerEvent = (eventInit) => ({\n    pointerId: 1,\n    pointerType: hasTouch() ? \"touch\" : \"mouse\",\n    ...mapNonBubblingMouseEvent(eventInit),\n});\n\n/**\n * @param {FullEventInit<WheelEventInit>} eventInit\n */\nconst mapWheelEvent = (eventInit) => ({\n    ...specialKeys,\n    ...mapBubblingEvent(eventInit),\n});\n\n// Touch event mappers\n// -------------------\n\n/**\n * @param {FullEventInit<TouchEventInit>} eventInit\n */\nconst mapCancelableTouchEvent = (eventInit) => {\n    const touches = eventInit.targetTouches ||\n        eventInit.touches || [new Touch({ identifier: 0, ...eventInit })];\n    return {\n        view: getWindow(),\n        ...mapBubblingCancelableEvent(eventInit),\n        changedTouches: eventInit.changedTouches || touches,\n        target: eventInit.target,\n        targetTouches: eventInit.targetTouches || touches,\n        touches: eventInit.touches || (eventInit.type === \"touchend\" ? [] : touches),\n    };\n};\n\n/**\n * @param {FullEventInit<TouchEventInit>} eventInit\n */\nconst mapNonCancelableTouchEvent = (eventInit) => ({\n    ...mapCancelableTouchEvent(eventInit),\n    cancelable: false,\n});\n\n// Keyboard & input event mappers\n// ------------------------------\n\n/**\n * @param {FullEventInit<InputEventInit>} eventInit\n */\nconst mapCancelableInputEvent = (eventInit) => ({\n    ...mapInputEvent(eventInit),\n    cancelable: true,\n});\n\n/**\n * @param {FullEventInit<InputEventInit>} eventInit\n */\nconst mapInputEvent = (eventInit) => ({\n    data: null,\n    isComposing: Boolean(runTime.isComposing),\n    view: getWindow(),\n    ...mapBubblingEvent(eventInit),\n});\n\n/**\n * @param {FullEventInit<KeyboardEventInit>} eventInit\n */\nconst mapKeyboardEvent = (eventInit) => ({\n    ...specialKeys,\n    isComposing: Boolean(runTime.isComposing),\n    view: getWindow(),\n    ...mapBubblingCancelableEvent(eventInit),\n});\n\n//-----------------------------------------------------------------------------\n// Exports\n//-----------------------------------------------------------------------------\n\n/**\n * Ensures that the given {@link AsyncTarget} is checked.\n *\n * If it is not checked, a click is simulated on the input.\n * If the input is still not checked after the click, an error is thrown.\n *\n * @see {@link click}\n * @param {AsyncTarget} target\n * @param {PointerOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  check(\"input[type=checkbox]\"); // Checks the first <input> checkbox element\n */\nexport async function check(target, options) {\n    const finalizeEvents = setupEvents(\"check\");\n    const element = queryFirst(await target, options);\n    if (!isCheckable(element)) {\n        throw new HootDomError(\n            `cannot call \\`check()\\`: target should be a checkbox or radio input`\n        );\n    }\n\n    const checkTarget = getTag(element) === \"label\" ? element.control : element;\n    if (!checkTarget.checked) {\n        await _implicitHover(element, options);\n        await _click(element, options);\n\n        if (!checkTarget.checked) {\n            throw new HootDomError(\n                `error when calling \\`check()\\`: target is not checked after interaction`\n            );\n        }\n    }\n\n    return finalizeEvents(options);\n}\n\n/**\n * Clears the **value** of the current **active element**.\n *\n * This is done using the following sequence:\n * - pressing \"Control\" + \"A\" to select the whole value;\n * - pressing \"Backspace\" to delete the value;\n * - (optional) triggering a \"change\" event by pressing \"Enter\".\n *\n * @param {FillOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  clear(); // Clears the value of the current active element\n */\nexport async function clear(options) {\n    const finalizeEvents = setupEvents(\"clear\");\n    const element = getActiveElement();\n\n    if (!hasTagName(element, \"select\") && !isEditable(element)) {\n        throw new HootDomError(\n            `cannot call \\`clear()\\`: target should be editable or a <select> element`\n        );\n    }\n\n    if (isEditable(element)) {\n        await _clear(element, options);\n    } else {\n        // Selects\n        await _select(element, \"\");\n    }\n\n    return finalizeEvents(options);\n}\n\n/**\n * Performs a click sequence on the given {@link AsyncTarget}.\n *\n * The event sequence is as follow:\n *  - `pointerdown`\n *  - [desktop] `mousedown`\n *  - [touch] `touchstart`\n *  - [target is not active element] `blur`\n *  - [target is focusable] `focus`\n *  - `pointerup`\n *  - [desktop] `mouseup`\n *  - [touch] `touchend`\n *  - `click`\n *  - `dblclick` if click is not prevented & current click count is even\n *\n * @param {AsyncTarget} target\n * @param {PointerOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  click(\"button\"); // Clicks on the first <button> element\n */\nexport async function click(target, options) {\n    const finalizeEvents = setupEvents(\"click\");\n    const element = queryFirst(await target, options);\n\n    await _implicitHover(element, options);\n    await _click(element, options);\n\n    return finalizeEvents(options);\n}\n\n/**\n * Performs two click sequences on the given {@link AsyncTarget}.\n *\n * @see {@link click}\n * @param {AsyncTarget} target\n * @param {PointerOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  dblclick(\"button\"); // Double-clicks on the first <button> element\n */\nexport async function dblclick(target, options) {\n    const finalizeEvents = setupEvents(\"dblclick\");\n    const element = queryFirst(await target, options);\n\n    await _implicitHover(element, options);\n    await _click(element, options);\n    await _click(element, options);\n\n    return finalizeEvents(options);\n}\n\n/**\n * Creates a new DOM {@link Event} of the given type and dispatches it on the given\n * {@link Target}.\n *\n * Note that this function is free of side-effects and does not trigger any other\n * event or special action. It also only supports standard DOM events, and will\n * crash when trying to dispatch a non-standard or deprecated event.\n *\n * @template {EventType} T\n * @param {EventTarget} target\n * @param {T} type\n * @param {EventInit} [eventInit]\n * @example\n * await dispatch(document.querySelector(\"input\"), \"paste\"); // Dispatches a \"paste\" event on the given <input>\n */\nexport async function dispatch(target, type, eventInit) {\n    if (type in DEPRECATED_EVENTS) {\n        throw new HootDomError(\n            `cannot dispatch \"${type}\" event: this event type is deprecated, use \"${DEPRECATED_EVENTS[type]}\" instead`\n        );\n    }\n    if (type !== type.toLowerCase()) {\n        throw new HootDomError(\n            `cannot dispatch \"${type}\" event: this event type is either non-standard or deprecated`\n        );\n    }\n    if (eventInit && typeof eventInit === \"object\") {\n        for (const key in eventInit) {\n            if (key in DEPRECATED_EVENT_PROPERTIES) {\n                throw new HootDomError(\n                    `cannot dispatch \"${type}\" event: property \"${key}\" is deprecated, use \"${DEPRECATED_EVENT_PROPERTIES[key]}\" instead`\n                );\n            }\n        }\n    }\n\n    const [Constructor, processParams] = getEventConstructor(type);\n    const params = processParams({ ...eventInit, target, type });\n    const event = new Constructor(type, params);\n\n    await Promise.resolve(target.dispatchEvent(event));\n\n    getCurrentEvents().push(event);\n\n    if (afterNextDispatch) {\n        const callback = afterNextDispatch;\n        afterNextDispatch = null;\n        await callback();\n    }\n\n    return event;\n}\n\n/**\n * Starts a drag sequence on the given {@link AsyncTarget}.\n *\n * Returns a set of helper functions to direct the sequence:\n * - `moveTo`: moves the pointer to the given target;\n * - `drop`: drops the dragged element on the given target (if any);\n * - `cancel`: cancels the drag sequence.\n *\n * @param {AsyncTarget} target\n * @param {PointerOptions} [options]\n * @returns {Promise<DragHelpers>}\n * @example\n *  drag(\".card:first\").drop(\".card:last\"); // Drags the first card onto the last one\n * @example\n *  drag(\".card:first\").moveTo(\".card:last\").drop(); // Same as above\n * @example\n *  const { cancel, moveTo } = await drag(\".card:first\"); // Starts the drag sequence\n *  moveTo(\".card:eq(3)\"); // Moves the dragged card to the 4th card\n *  cancel(); // Cancels the drag sequence\n */\nexport async function drag(target, options) {\n    /**\n     * @template T\n     * @param {T} fn\n     * @param {boolean} endDrag\n     * @returns {T}\n     */\n    const expectIsDragging = (fn, endDrag) => {\n        return {\n            async [fn.name](...args) {\n                if (dragEndReason) {\n                    throw new HootDomError(\n                        `cannot execute drag helper \\`${fn.name}\\`: drag sequence has been ended by \\`${dragEndReason}\\``\n                    );\n                }\n                const result = await fn(...args);\n                if (endDrag) {\n                    dragEndReason = fn.name;\n                }\n                return result;\n            },\n        }[fn.name];\n    };\n\n    const cancel = expectIsDragging(\n        /** @type {DragHelpers[\"cancel\"]} */\n        async function cancel(options) {\n            const finalizeEvents = setupEvents(\"drag & drop: cancel\");\n\n            await _press(getDocument().body, { key: \"Escape\" });\n\n            dragEvents.push(...(await finalizeEvents(options)));\n\n            return dragEvents;\n        },\n        true\n    );\n\n    const drop = expectIsDragging(\n        /** @type {DragHelpers[\"drop\"]} */\n        async function drop(to, options) {\n            if (to) {\n                await moveTo(to, options);\n            }\n\n            const finalizeEvents = setupEvents(\"drag & drop: drop\");\n\n            await _pointerUp(runTime.pointerTarget, options);\n\n            dragEvents.push(...(await finalizeEvents(options)));\n\n            return dragEvents;\n        },\n        true\n    );\n\n    const moveTo = expectIsDragging(\n        /** @type {DragHelpers[\"moveTo\"]} */\n        async function moveTo(to, options) {\n            const finalizeEvents = setupEvents(\"drag & drop: move\");\n\n            await _hover(queryFirst(await to), options);\n\n            dragEvents.push(...(await finalizeEvents(options)));\n\n            return dragHelpers;\n        },\n        false\n    );\n\n    const finalizeEvents = setupEvents(\"drag & drop: start\");\n    const dragHelpers = { cancel, drop, moveTo };\n    const element = queryFirst(await target);\n\n    let dragEndReason = null;\n\n    // Pointer down on main target\n    await _implicitHover(element, options);\n    await _pointerDown(element, options);\n\n    const dragEvents = await finalizeEvents(options);\n\n    return dragHelpers;\n}\n\n/**\n * Combination of {@link clear} and {@link fill}:\n * - first, clears the input value (if any)\n * - then fills the input with the given value\n *\n * @see {@link clear}\n * @see {@link fill}\n * @param {InputValue} value\n * @param {FillOptions} options\n * @returns {Promise<EventList>}\n * @example\n *  fill(\"foo\"); // Types \"foo\" in the active element\n *  edit(\"Hello World\"); // Replaces \"foo\" by \"Hello World\"\n */\nexport async function edit(value, options) {\n    const finalizeEvents = setupEvents(\"edit\");\n    const element = getActiveElement();\n    if (!isEditable(element)) {\n        throw new HootDomError(`cannot call \\`edit()\\`: target should be editable`);\n    }\n\n    if (getNodeValue(element)) {\n        await _clear(element);\n    }\n    await _fill(element, value, options);\n\n    return finalizeEvents(options);\n}\n\n/**\n * @param {boolean} toggle\n */\nexport function enableEventLogs(toggle) {\n    allowLogs = toggle ?? true;\n}\n\n/**\n * Fills the current active element with the given `value`. This helper is intended\n * for `<input>` and `<textarea>` elements, with the exception of `\"checkbox\"` and\n * `\"radio\"` types, which should be selected using the {@link check} helper.\n *\n * If the target is an editable input, its string `value` will be input one character\n * at a time, each generating its corresponding keyboard event sequence. This behavior\n * can be overriden by passing the `instantly` option, which will instead simulate\n * a `control` + `v` keyboard sequence, resulting in the whole text being pasted.\n *\n * Note that the given value is **appended** to the current value of the element.\n *\n * If the active element is a `<input type=\"file\"/>`, the `value` should be a\n * `File`/list of `File` object(s).\n *\n * @param {InputValue} value\n * @param {FillOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  fill(\"Hello World\"); // Types \"Hello World\" in the active element\n * @example\n *  fill(\"Hello World\", { instantly: true }); // Pastes \"Hello World\" in the active element\n * @example\n *  fill(new File([\"Hello World\"], \"hello.txt\")); // Uploads a file named \"hello.txt\" with \"Hello World\" as content\n */\nexport async function fill(value, options) {\n    const finalizeEvents = setupEvents(\"fill\");\n    const element = getActiveElement();\n\n    if (!isEditable(element)) {\n        throw new HootDomError(`cannot call \\`fill()\\`: target should be editable`);\n    }\n\n    await _fill(element, value, options);\n\n    return finalizeEvents(options);\n}\n\n/**\n * Performs a hover sequence on the given {@link AsyncTarget}.\n *\n * The event sequence is as follow:\n *  - `pointerover`\n *  - [desktop] `mouseover`\n *  - `pointerenter`\n *  - [desktop] `mouseenter`\n *  - `pointermove`\n *  - [desktop] `mousemove`\n *  - [touch] `touchmove`\n *\n * @param {AsyncTarget} target\n * @param {PointerOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  hover(\"button\"); // Hovers the first <button> element\n */\nexport async function hover(target, options) {\n    const finalizeEvents = setupEvents(\"hover\");\n    const element = queryFirst(await target, options);\n\n    await _hover(element, options);\n\n    return finalizeEvents(options);\n}\n\n/**\n * Performs a key down sequence on the given {@link Target}.\n *\n * The event sequence is as follow:\n *  - `keydown`\n *\n * Additional actions will be performed depending on the key pressed:\n * - `Tab`: focus next (or previous with `shift`) focusable element;\n * - `c`: copy current selection to clipboard;\n * - `v`: paste current clipboard content to current element;\n * - `Enter`: submit the form if the target is a `<button type=\"button\">` or\n *  a `<form>` element, or trigger a `change` event on the target if it is\n *  an `<input>` element;\n * - `Space`: trigger a `click` event on the target if it is an `<input type=\"checkbox\">`\n *  element.\n *\n * @param {KeyStrokes} keyStrokes\n * @param {KeyboardOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  keyDown(\" \"); // Space key\n */\nexport async function keyDown(keyStrokes, options) {\n    const finalizeEvents = setupEvents(\"keyDown\");\n    const eventInits = parseKeyStrokes(keyStrokes, options);\n    for (const eventInit of eventInits) {\n        await _keyDown(getActiveElement(), eventInit);\n    }\n\n    return finalizeEvents(options);\n}\n\n/**\n * Performs a key up sequence on the given {@link Target}.\n *\n * The event sequence is as follow:\n *  - `keyup`\n *\n * @param {KeyStrokes} keyStrokes\n * @param {KeyboardOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  keyUp(\"Enter\");\n */\nexport async function keyUp(keyStrokes, options) {\n    const finalizeEvents = setupEvents(\"keyUp\");\n    const eventInits = parseKeyStrokes(keyStrokes, options);\n    for (const eventInit of eventInits) {\n        await _keyUp(getActiveElement(), eventInit);\n    }\n\n    return finalizeEvents(options);\n}\n\n/**\n * Performs a leave sequence on the given {@link Target}.\n *\n * The event sequence is as follow:\n *  - `pointermove`\n *  - [desktop] `mousemove`\n *  - [touch] `touchmove`\n *  - `pointerout`\n *  - [desktop] `mouseout`\n *  - `pointerleave`\n *  - [desktop] `mouseleave`\n *\n * @param {PointerOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  leave(\"button\"); // Moves out of <button>\n */\nexport async function leave(options) {\n    const finalizeEvents = setupEvents(\"leave\");\n\n    await _hover(null, options);\n\n    return finalizeEvents(options);\n}\n\n/**\n * Shorthand helper to attach an event listener to the given {@link Target}, and\n * returning a function to remove the listener.\n *\n * @template {EventType} T\n * @param {Target<EventTarget>} target\n * @param {T} type\n * @param {(event: GlobalEventHandlersEventMap[T]) => any} listener\n * @param {boolean | AddEventListenerOptions} [options]\n * @returns {() => void}\n * @example\n *  const off = on(\"button\", \"click\", onClick);\n *  after(off);\n */\nexport function on(target, type, listener, options) {\n    const targets = isEventTarget(target) ? [target] : queryAll(target);\n    if (!targets.length) {\n        throw new HootDomError(`expected at least 1 event target, got none`);\n    }\n    for (const eventTarget of targets) {\n        eventTarget.addEventListener(type, listener, options);\n    }\n\n    return function off() {\n        for (const eventTarget of targets) {\n            eventTarget.removeEventListener(type, listener, options);\n        }\n    };\n}\n\n/**\n * Performs a pointer down on the given {@link AsyncTarget}.\n *\n * The event sequence is as follow:\n *  - `pointerdown`\n *  - [desktop] `mousedown`\n *  - [touch] `touchstart`\n *  - [target is not active element] `blur`\n *  - [target is focusable] `focus`\n *\n * @param {AsyncTarget} target\n * @param {PointerOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  pointerDown(\"button\"); // Focuses to the first <button> element\n */\nexport async function pointerDown(target, options) {\n    const finalizeEvents = setupEvents(\"pointerDown\");\n    const element = queryFirst(await target, options);\n\n    await _implicitHover(element, options);\n    await _pointerDown(element, options);\n\n    return finalizeEvents(options);\n}\n\n/**\n * Performs a pointer up on the given {@link AsyncTarget}.\n *\n * The event sequence is as follow:\n * - `pointerup`\n * - [desktop] `mouseup`\n * - [touch] `touchend`\n *\n * @param {AsyncTarget} target\n * @param {PointerOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  pointerUp(\"body\"); // Triggers a pointer up on the <body> element\n */\nexport async function pointerUp(target, options) {\n    const finalizeEvents = setupEvents(\"pointerUp\");\n    const element = queryFirst(await target, options);\n\n    await _implicitHover(element, options);\n    await _pointerUp(element, options);\n\n    return finalizeEvents(options);\n}\n\n/**\n * Performs a keyboard event sequence on the given {@link Target}.\n *\n * The event sequence is as follow:\n *  - `keydown`\n *  - `keyup`\n *\n * @param {KeyStrokes} keyStrokes\n * @param {KeyboardOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  pointerDown(\"button[type=submit]\"); // Moves focus to <button>\n *  keyDown(\"Enter\"); // Submits the form\n * @example\n *  keyDown(\"Shift+Tab\"); // Focuses previous focusable element\n * @example\n *  keyDown([\"ctrl\", \"v\"]); // Pastes current clipboard content\n */\nexport async function press(keyStrokes, options) {\n    const finalizeEvents = setupEvents(\"press\");\n    const eventInits = parseKeyStrokes(keyStrokes, options);\n    const activeElement = getActiveElement();\n\n    for (const eventInit of eventInits) {\n        await _keyDown(activeElement, eventInit);\n    }\n    for (const eventInit of eventInits.reverse()) {\n        await _keyUp(activeElement, eventInit);\n    }\n\n    return finalizeEvents(options);\n}\n\n/**\n * Performs a resize event sequence on the given {@link Target}.\n *\n * The event sequence is as follow:\n *  - `resize`\n *\n * The target will be resized to the given dimensions, enforced by `!important` style\n * attributes.\n *\n * @param {Dimensions} dimensions\n * @param {EventOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  resize(\"body\", { width: 1000, height: 500 }); // Resizes <body> to 1000x500\n */\nexport async function resize(dimensions, options) {\n    const finalizeEvents = setupEvents(\"resize\");\n    const [width, height] = parseDimensions(dimensions);\n\n    setDimensions(width, height);\n\n    await dispatch(getWindow(), \"resize\");\n\n    return finalizeEvents(options);\n}\n\n/**\n * Performs a scroll event sequence on the given {@link AsyncTarget}.\n *\n * The event sequence is as follow:\n *  - [desktop] `wheel`\n *  - `scroll`\n *\n * @param {AsyncTarget} target\n * @param {Position} position\n * @param {EventOptions & QueryOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  scroll(\"body\", { y: 0 }); // Scrolls to the top of <body>\n */\nexport async function scroll(target, position, options) {\n    const finalizeEvents = setupEvents(\"scroll\");\n\n    /** @type {ScrollToOptions} */\n    const scrollOptions = {};\n    const [x, y] = parsePosition(position);\n    if (!$isNaN(x)) {\n        scrollOptions.left = x;\n    }\n    if (!$isNaN(y)) {\n        scrollOptions.top = y;\n    }\n    const element = queryFirst(await target, { ...options, scrollable: true });\n    if (!hasTouch()) {\n        await dispatch(element, \"wheel\");\n    }\n    // This will trigger a trusted \"scroll\" event\n    catchNextEvent(element, \"scroll\");\n    await Promise.resolve(element.scrollTo(scrollOptions));\n\n    return finalizeEvents(options);\n}\n\n/**\n * Performs a selection event sequence current active element. This helper is intended\n * for `<select>` elements only.\n *\n * The event sequence is as follow:\n *  - `change`\n *\n * @param {string | number | (string | number)[]} value\n * @param {SelectOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  click(\"select[name=country]\"); // Focuses <select> element\n *  select(\"belgium\"); // Selects the <option value=\"belgium\"> element\n */\nexport async function select(value, options) {\n    const finalizeEvents = setupEvents(\"select\");\n    const element = options?.target ? queryFirst(await options.target) : getActiveElement();\n\n    if (!hasTagName(element, \"select\")) {\n        throw new HootDomError(`cannot call \\`select()\\`: target should be a <select> element`);\n    }\n\n    if (options?.target) {\n        await _implicitHover(element);\n        await _pointerDown(element);\n    }\n    await _select(element, value);\n    if (options?.target) {\n        await _pointerUp(element);\n    }\n\n    return finalizeEvents(options);\n}\n\n/**\n * Gives the given {@link File} list to the current file input. This helper only\n * works if a file input has been previously interacted with (by clicking on it).\n *\n * @param {MaybeIterable<File>} files\n * @param {EventOptions} [options]\n * @returns {Promise<EventList>}\n */\nexport async function setInputFiles(files, options) {\n    if (!runTime.fileInput) {\n        throw new HootDomError(\n            `cannot call \\`setInputFiles()\\`: no file input has been interacted with`\n        );\n    }\n\n    const finalizeEvents = setupEvents(\"setInputFiles\");\n\n    await _fill(runTime.fileInput, files);\n\n    runTime.fileInput = null;\n\n    return finalizeEvents(options);\n}\n\n/**\n * Sets the given value to the given \"input[type=range]\" {@link Target}.\n *\n * The event sequence is as follow:\n *  - `pointerdown`\n *  - `input`\n *  - `change`\n *  - `pointerup`\n *\n * @param {AsyncTarget} target\n * @param {number} value\n * @param {PointerOptions} [options]\n * @returns {Promise<EventList>}\n */\nexport async function setInputRange(target, value, options) {\n    const finalizeEvents = setupEvents(\"setInputRange\");\n    const element = queryFirst(await target, options);\n\n    await _implicitHover(element, options);\n    await _pointerDown(element, options);\n    await _fill(element, value);\n    await _pointerUp(element, options);\n\n    return finalizeEvents(options);\n}\n\n/**\n * @param {HTMLElement} fixture\n */\nexport function setupEventActions(fixture) {\n    if (runTime.pointerDownTimeout) {\n        globalThis.clearTimeout(runTime.pointerDownTimeout);\n    }\n\n    removeChangeTargetListeners();\n\n    fixture.addEventListener(\"click\", registerFileInput, { capture: true });\n\n    // Runtime global variables\n    $assign(runTime, getDefaultRunTimeValue());\n\n    // Special keys\n    $assign(specialKeys, getDefaultSpecialKeysValue());\n}\n\n/**\n * Ensures that the given {@link AsyncTarget} is unchecked.\n *\n * If it is checked, a click is triggered on the input.\n * If the input is still checked after the click, an error is thrown.\n *\n * @see {@link click}\n * @param {AsyncTarget} target\n * @param {PointerOptions} [options]\n * @returns {Promise<EventList>}\n * @example\n *  uncheck(\"input[type=checkbox]\"); // Unchecks the first <input> checkbox element\n */\nexport async function uncheck(target, options) {\n    const finalizeEvents = setupEvents(\"uncheck\");\n    const element = queryFirst(await target, options);\n    if (!isCheckable(element)) {\n        throw new HootDomError(\n            `cannot call \\`uncheck()\\`: target should be a checkbox or radio input`\n        );\n    }\n\n    const checkTarget = getTag(element) === \"label\" ? element.control : element;\n    if (checkTarget.checked) {\n        await _implicitHover(element, options);\n        await _click(element, options);\n\n        if (checkTarget.checked) {\n            throw new HootDomError(\n                `error when calling \\`uncheck()\\`: target is still checked after interaction`\n            );\n        }\n    }\n\n    return finalizeEvents(options);\n}\n\n/**\n * Triggers a \"beforeunload\" event the current window.\n *\n * @param {EventOptions} [options]\n * @returns {Promise<EventList>}\n */\nexport async function unload(options) {\n    const finalizeEvents = setupEvents(\"unload\");\n\n    await dispatch(getWindow(), \"beforeunload\");\n\n    return finalizeEvents(options);\n}\n\n/** @extends {Array<Event>} */\nexport class EventList extends Array {\n    constructor(...args) {\n        super(...args.flat());\n    }\n\n    /**\n     * @param {EventListPredicate} predicate\n     */\n    get(predicate) {\n        return this.getAll(predicate)[0] || null;\n    }\n\n    /**\n     * @param {EventListPredicate} predicate\n     */\n    getAll(predicate) {\n        if (typeof predicate !== \"function\") {\n            const type = predicate;\n            predicate = (ev) => ev.type === type;\n        }\n        return this.filter(predicate);\n    }\n}\n", "/** @odoo-module */\n\nimport { HootDomError } from \"../hoot_dom_utils\";\n\n/**\n * @typedef {{\n *  message?: string | () => string;\n *  timeout?: number;\n * }} WaitOptions\n */\n\n//-----------------------------------------------------------------------------\n// Global\n//-----------------------------------------------------------------------------\n\nconst {\n    cancelAnimationFrame,\n    clearInterval,\n    clearTimeout,\n    Error,\n    Math: { ceil: $ceil, floor: $floor, max: $max, min: $min },\n    performance,\n    Promise,\n    requestAnimationFrame,\n    setInterval,\n    setTimeout,\n} = globalThis;\n/** @type {Performance[\"now\"]} */\nconst $performanceNow = performance.now.bind(performance);\n\n//-----------------------------------------------------------------------------\n// Internal\n//-----------------------------------------------------------------------------\n\n/**\n * @param {number} id\n */\nconst animationToId = (id) => ID_PREFIX.animation + String(id);\n\nconst getNextTimerValues = () => {\n    /** @type {[number, () => any, string] | null} */\n    let timerValues = null;\n    for (const [internalId, [callback, init, delay]] of timers.entries()) {\n        const timeout = init + delay;\n        if (!timerValues || timeout < timerValues[0]) {\n            timerValues = [timeout, callback, internalId];\n        }\n    }\n    return timerValues;\n};\n\n/**\n * @param {string} id\n */\nconst idToAnimation = (id) => Number(id.slice(ID_PREFIX.animation.length));\n\n/**\n * @param {string} id\n */\nconst idToInterval = (id) => Number(id.slice(ID_PREFIX.interval.length));\n\n/**\n * @param {string} id\n */\nconst idToTimeout = (id) => Number(id.slice(ID_PREFIX.timeout.length));\n\n/**\n * @param {number} id\n */\nconst intervalToId = (id) => ID_PREFIX.interval + String(id);\n\nconst now = () => $performanceNow() + timeOffset;\n\n/**\n * @param {number} id\n */\nconst timeoutToId = (id) => ID_PREFIX.timeout + String(id);\n\nconst ID_PREFIX = {\n    animation: \"a_\",\n    interval: \"i_\",\n    timeout: \"t_\",\n};\n\n/** @type {Map<string, [() => any, number, number]>} */\nconst timers = new Map();\n\nlet allowTimers = true;\nlet freezed = false;\nlet frameDelay = 1000 / 60;\nlet nextDummyId = 1;\nlet timeOffset = 0;\n\n//-----------------------------------------------------------------------------\n// Exports\n//-----------------------------------------------------------------------------\n\n/**\n * @param {number} [frameCount]\n */\nexport function advanceFrame(frameCount) {\n    return advanceTime(frameDelay * $max(1, frameCount));\n}\n\n/**\n * Advances the current time by the given amount of milliseconds. This will\n * affect all timeouts, intervals, animations and date objects.\n *\n * It returns a promise resolved after all related callbacks have been executed.\n *\n * @param {number} ms\n * @returns {Promise<number>} time consumed by timers (in ms).\n */\nexport function advanceTime(ms) {\n    const targetTime = now() + ms;\n    let remaining = ms;\n    /** @type {ReturnType<typeof getNextTimerValues>} */\n    let timerValues;\n    while ((timerValues = getNextTimerValues()) && timerValues[0] <= targetTime) {\n        const [timeout, handler, id] = timerValues;\n        const diff = timeout - now();\n        if (diff > 0) {\n            timeOffset += $min(remaining, diff);\n            remaining = $max(remaining - diff, 0);\n        }\n        if (timers.has(id)) {\n            handler(timeout);\n        }\n    }\n\n    if (remaining > 0) {\n        timeOffset += remaining;\n    }\n\n    // Waits for callbacks to execute\n    return animationFrame().then(() => ms);\n}\n\n/**\n * Returns a promise resolved after the next animation frame, typically allowing\n * Owl components to render.\n *\n * @returns {Deferred<void>}\n */\nexport function animationFrame() {\n    return new Deferred((resolve) => requestAnimationFrame(() => delay().then(resolve)));\n}\n\n/**\n * Cancels all current timeouts, intervals and animations.\n */\nexport function cancelAllTimers() {\n    for (const id of timers.keys()) {\n        if (id.startsWith(ID_PREFIX.animation)) {\n            globalThis.cancelAnimationFrame(idToAnimation(id));\n        } else if (id.startsWith(ID_PREFIX.interval)) {\n            globalThis.clearInterval(idToInterval(id));\n        } else if (id.startsWith(ID_PREFIX.timeout)) {\n            globalThis.clearTimeout(idToTimeout(id));\n        }\n    }\n}\n\nexport function cleanupTime() {\n    cancelAllTimers();\n\n    freezed = false;\n}\n\n/**\n * Returns a promise resolved after a given amount of milliseconds (default to 0).\n *\n * @param {number} [duration]\n * @returns {Deferred<void>}\n * @example\n *  await delay(1000); // waits for 1 second\n */\nexport function delay(duration) {\n    return new Deferred((resolve) => setTimeout(resolve, duration));\n}\n\n/**\n * @param {boolean} setFreeze\n */\nexport function freezeTime(setFreeze) {\n    freezed = setFreeze ?? !freezed;\n}\n\nexport function getTimeOffset() {\n    return timeOffset;\n}\n\nexport function isTimeFreezed() {\n    return freezed;\n}\n\n/**\n * Returns a promise resolved after the next microtask tick.\n *\n * @returns {Promise<void>}\n */\nexport function microTick() {\n    return Deferred.resolve();\n}\n\n/** @type {typeof cancelAnimationFrame} */\nexport function mockedCancelAnimationFrame(handle) {\n    if (!freezed) {\n        cancelAnimationFrame(handle);\n    }\n    timers.delete(animationToId(handle));\n}\n\n/** @type {typeof clearInterval} */\nexport function mockedClearInterval(intervalId) {\n    if (!freezed) {\n        clearInterval(intervalId);\n    }\n    timers.delete(intervalToId(intervalId));\n}\n\n/** @type {typeof clearTimeout} */\nexport function mockedClearTimeout(timeoutId) {\n    if (!freezed) {\n        clearTimeout(timeoutId);\n    }\n    timers.delete(timeoutToId(timeoutId));\n}\n\n/** @type {typeof requestAnimationFrame} */\nexport function mockedRequestAnimationFrame(callback) {\n    if (!allowTimers) {\n        return 0;\n    }\n\n    const handler = () => {\n        mockedCancelAnimationFrame(handle);\n        return callback(now());\n    };\n\n    const animationValues = [handler, now(), frameDelay];\n    const handle = freezed ? nextDummyId++ : requestAnimationFrame(handler);\n    const internalId = animationToId(handle);\n    timers.set(internalId, animationValues);\n\n    return handle;\n}\n\n/** @type {typeof setInterval} */\nexport function mockedSetInterval(callback, ms, ...args) {\n    if (!allowTimers) {\n        return 0;\n    }\n\n    if (isNaN(ms) || !ms || ms < 0) {\n        ms = 0;\n    }\n\n    const handler = () => {\n        if (allowTimers) {\n            intervalValues[1] = Math.max(now(), intervalValues[1] + ms);\n        } else {\n            mockedClearInterval(intervalId);\n        }\n        return callback(...args);\n    };\n\n    const intervalValues = [handler, now(), ms];\n    const intervalId = freezed ? nextDummyId++ : setInterval(handler, ms);\n    const internalId = intervalToId(intervalId);\n    timers.set(internalId, intervalValues);\n\n    return intervalId;\n}\n\n/** @type {typeof setTimeout} */\nexport function mockedSetTimeout(callback, ms, ...args) {\n    if (!allowTimers) {\n        return 0;\n    }\n\n    if (isNaN(ms) || !ms || ms < 0) {\n        ms = 0;\n    }\n\n    const handler = () => {\n        mockedClearTimeout(timeoutId);\n        return callback(...args);\n    };\n\n    const timeoutValues = [handler, now(), ms];\n    const timeoutId = freezed ? nextDummyId++ : setTimeout(handler, ms);\n    const internalId = timeoutToId(timeoutId);\n    timers.set(internalId, timeoutValues);\n\n    return timeoutId;\n}\n\nexport function resetTimeOffset() {\n    timeOffset = 0;\n}\n\n/**\n * Calculates the amount of time needed to run all current timeouts, intervals and\n * animations, and then advances the current time by that amount.\n *\n * @see {@link advanceTime}\n * @param {boolean} [preventTimers=false]\n * @returns {Promise<number>} time consumed by timers (in ms).\n */\nexport async function runAllTimers(preventTimers = false) {\n    if (!timers.size) {\n        return 0;\n    }\n\n    if (preventTimers) {\n        allowTimers = false;\n    }\n\n    const endts = $max(...[...timers.values()].map(([, init, delay]) => init + delay));\n    const ms = await advanceTime($ceil(endts - now()));\n\n    if (preventTimers) {\n        allowTimers = true;\n    }\n\n    return ms;\n}\n\n/**\n * Sets the current frame rate (in fps) used by animation frames (default to 60fps).\n *\n * @param {number} frameRate\n */\nexport function setFrameRate(frameRate) {\n    if (!Number.isInteger(frameRate) || frameRate <= 0 || frameRate > 1000) {\n        throw new Error(\"frame rate must be an number between 1 and 1000\");\n    }\n    frameDelay = 1000 / frameRate;\n}\n\n/**\n * Returns a promise resolved after the next task tick.\n *\n * @returns {Deferred<void>}\n */\nexport function tick() {\n    return delay();\n}\n\n/**\n * Returns a promise fulfilled when the given `predicate` returns a truthy value,\n * with the value of the promise being the return value of the `predicate`.\n *\n * The `predicate` is run once initially, and then on each animation frame until\n * it succeeds or fail.\n *\n * The promise automatically rejects after a given `timeout` (defaults to 5 seconds).\n *\n * @template T\n * @param {() => T} predicate\n * @param {WaitOptions} [options]\n * @returns {Deferred<T>}\n * @example\n *  await waitUntil(() => []); // -> []\n * @example\n *  const button = await waitUntil(() => queryOne(\"button:visible\"));\n *  button.click();\n */\nexport function waitUntil(predicate, options) {\n    // Early check before running the loop\n    const result = predicate();\n    if (result) {\n        return Deferred.resolve(result);\n    }\n\n    let handle;\n    let timeoutId;\n    let running = true;\n    return new Deferred((resolve, reject) => {\n        const runCheck = () => {\n            const result = predicate();\n            if (result) {\n                resolve(result);\n            } else if (running) {\n                handle = requestAnimationFrame(runCheck);\n            } else {\n                let message =\n                    options?.message || `'waitUntil' timed out after %timeout% milliseconds`;\n                if (typeof message === \"function\") {\n                    message = message();\n                }\n                reject(new HootDomError(message.replace(\"%timeout%\", String(timeout))));\n            }\n        };\n\n        const timeout = $floor(options?.timeout ?? 200);\n        timeoutId = setTimeout(() => (running = false), timeout);\n        handle = requestAnimationFrame(runCheck);\n    }).finally(() => {\n        cancelAnimationFrame(handle);\n        clearTimeout(timeoutId);\n    });\n}\n\n/**\n * Manually resolvable and rejectable promise. It introduces 2 new methods:\n *  - {@link reject} rejects the deferred with the given reason;\n *  - {@link resolve} resolves the deferred with the given value.\n *\n * @template [T=unknown]\n */\nexport class Deferred extends Promise {\n    /** @type {typeof Promise.resolve<T>} */\n    _resolve;\n    /** @type {typeof Promise.reject<T>} */\n    _reject;\n\n    /**\n     * @param {(resolve: (value?: T) => any, reject: (reason?: any) => any) => any} [executor]\n     */\n    constructor(executor) {\n        let _resolve, _reject;\n\n        super((resolve, reject) => {\n            _resolve = resolve;\n            _reject = reject;\n            executor?.(_resolve, _reject);\n        });\n\n        this._resolve = _resolve;\n        this._reject = _reject;\n    }\n\n    /**\n     * @param {any} [reason]\n     */\n    async reject(reason) {\n        return this._reject(reason);\n    }\n\n    /**\n     * @param {T} [value]\n     */\n    async resolve(value) {\n        return this._resolve(value);\n    }\n}\n", "/** @odoo-module alias=@odoo/hoot-dom default=false */\n\n/**\n * @typedef {import(\"./helpers/dom\").Dimensions} Dimensions\n * @typedef {import(\"./helpers/dom\").FormatXmlOptions} FormatXmlOptions\n * @typedef {import(\"./helpers/dom\").Position} Position\n * @typedef {import(\"./helpers/dom\").QueryOptions} QueryOptions\n * @typedef {import(\"./helpers/dom\").QueryRectOptions} QueryRectOptions\n * @typedef {import(\"./helpers/dom\").QueryTextOptions} QueryTextOptions\n * @typedef {import(\"./helpers/dom\").Target} Target\n *\n * @typedef {import(\"./helpers/events\").DragHelpers} DragHelpers\n * @typedef {import(\"./helpers/events\").EventType} EventType\n * @typedef {import(\"./helpers/events\").FillOptions} FillOptions\n * @typedef {import(\"./helpers/events\").InputValue} InputValue\n * @typedef {import(\"./helpers/events\").KeyStrokes} KeyStrokes\n * @typedef {import(\"./helpers/events\").PointerOptions} PointerOptions\n */\n\nexport {\n    formatXml,\n    getActiveElement,\n    getFocusableElements,\n    getNextFocusableElement,\n    getParentFrame,\n    getPreviousFocusableElement,\n    isDisplayed,\n    isEditable,\n    isFocusable,\n    isInDOM,\n    isInViewPort,\n    isScrollable,\n    isVisible,\n    matches,\n    observe,\n    queryAll,\n    queryAllAttributes,\n    queryAllProperties,\n    queryAllRects,\n    queryAllTexts,\n    queryAllValues,\n    queryAttribute,\n    queryFirst,\n    queryOne,\n    queryRect,\n    queryText,\n    queryValue,\n    waitFor,\n    waitForNone,\n} from \"./helpers/dom\";\nexport {\n    check,\n    clear,\n    click,\n    dblclick,\n    drag,\n    edit,\n    fill,\n    hover,\n    keyDown,\n    keyUp,\n    leave,\n    dispatch as manuallyDispatchProgrammaticEvent,\n    on,\n    pointerDown,\n    pointerUp,\n    press,\n    resize,\n    scroll,\n    select,\n    setInputFiles,\n    setInputRange,\n    uncheck,\n    unload,\n} from \"./helpers/events\";\nexport {\n    Deferred,\n    advanceFrame,\n    advanceTime,\n    animationFrame,\n    cancelAllTimers,\n    delay,\n    freezeTime,\n    microTick,\n    runAllTimers,\n    setFrameRate,\n    tick,\n    waitUntil,\n} from \"./helpers/time\";\n", "/** @odoo-module */\n\n/**\n * @typedef {ArgumentPrimitive | `${ArgumentPrimitive}[]` | null} ArgumentType\n *\n * @typedef {\"any\"\n *  | \"bigint\"\n *  | \"boolean\"\n *  | \"error\"\n *  | \"function\"\n *  | \"integer\"\n *  | \"node\"\n *  | \"number\"\n *  | \"object\"\n *  | \"regex\"\n *  | \"string\"\n *  | \"symbol\"\n *  | \"undefined\"} ArgumentPrimitive\n */\n\n/**\n * @template T\n * @typedef {T | Iterable<T>} MaybeIterable\n */\n\n/**\n * @template T\n * @typedef {T | PromiseLike<T>} MaybePromise\n */\n\n//-----------------------------------------------------------------------------\n// Global\n//-----------------------------------------------------------------------------\n\nconst {\n    Boolean,\n    navigator: { userAgent: $userAgent },\n    RegExp,\n    SyntaxError,\n} = globalThis;\n\n//-----------------------------------------------------------------------------\n// Internal\n//-----------------------------------------------------------------------------\n\nconst R_REGEX_PATTERN = /^\\/(.*)\\/([dgimsuvy]+)?$/;\n\n//-----------------------------------------------------------------------------\n// Exports\n//-----------------------------------------------------------------------------\n\n/**\n * @param {Node} node\n */\nexport function getTag(node) {\n    return node?.nodeName.toLowerCase() || \"\";\n}\n\n/**\n * @returns {boolean}\n */\nexport function isFirefox() {\n    return /firefox/i.test($userAgent);\n}\n\n/**\n * Returns whether the given object is iterable (*excluding strings*).\n *\n * @template T\n * @template {T | Iterable<T>} V\n * @param {V} object\n * @returns {V extends Iterable<T> ? true : false}\n */\nexport function isIterable(object) {\n    return Boolean(object && typeof object === \"object\" && object[Symbol.iterator]);\n}\n\n/**\n * @param {string} filter\n * @returns {boolean}\n */\nexport function isRegExpFilter(filter) {\n    return R_REGEX_PATTERN.test(filter);\n}\n\n/**\n * @param {string} value\n * @param {{ safe?: boolean }} [options]\n * @returns {string | RegExp}\n */\nexport function parseRegExp(value, options) {\n    const regexParams = value.match(R_REGEX_PATTERN);\n    if (regexParams) {\n        const unified = regexParams[1].replace(/\\s+/g, \"\\\\s+\");\n        const flag = regexParams[2] || \"i\";\n        try {\n            return new RegExp(unified, flag);\n        } catch (error) {\n            if (error instanceof SyntaxError && options?.safe) {\n                return value;\n            } else {\n                throw error;\n            }\n        }\n    }\n    return value;\n}\n\n/**\n * @param {Node} node\n * @param {{ raw?: boolean }} [options]\n */\nexport function toSelector(node, options) {\n    const tagName = getTag(node);\n    const id = node.id ? `#${node.id}` : \"\";\n    const classNames = node.classList\n        ? [...node.classList].map((className) => `.${className}`)\n        : [];\n    if (options?.raw) {\n        return { tagName, id, classNames };\n    } else {\n        return [tagName, id, ...classNames].join(\"\");\n    }\n}\n\nexport class HootDomError extends Error {\n    name = \"HootDomError\";\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Attachment, FileSelector, IMAGE_MIMETYPES } from \"./file_selector\";\n\nexport class DocumentAttachment extends Attachment {\n    static template = \"html_editor.DocumentAttachment\";\n}\n\nexport class DocumentSelector extends FileSelector {\n    static mediaSpecificClasses = [\"o_image\"];\n    static mediaSpecificStyles = [];\n    static mediaExtraClasses = [];\n    static tagNames = [\"A\"];\n    static attachmentsListTemplate = \"html_editor.DocumentsListTemplate\";\n    static components = {\n        ...FileSelector.components,\n        DocumentAttachment,\n    };\n\n    setup() {\n        super.setup();\n\n        this.uploadText = _t(\"Upload a document\");\n        this.urlPlaceholder = \"https://www.odoo.com/mydocument\";\n        this.addText = _t(\"Add URL\");\n        this.searchPlaceholder = _t(\"Search a document\");\n        this.allLoadedText = _t(\"All documents have been loaded\");\n    }\n\n    get attachmentsDomain() {\n        const domain = super.attachmentsDomain;\n        domain.push([\"mimetype\", \"not in\", IMAGE_MIMETYPES]);\n        // The assets should not be part of the documents.\n        // All assets begin with '/web/assets/', see _get_asset_template_url().\n        domain.unshift(\"&\", \"|\", [\"url\", \"=\", null], \"!\", [\"url\", \"=like\", \"/web/assets/%\"]);\n        return domain;\n    }\n\n    async onClickDocument(document) {\n        this.selectAttachment(document);\n        await this.props.save();\n    }\n\n    async fetchAttachments(...args) {\n        const attachments = await super.fetchAttachments(...args);\n\n        if (this.selectInitialMedia()) {\n            for (const attachment of attachments) {\n                if (\n                    `/web/content/${attachment.id}` ===\n                    this.props.media.getAttribute(\"href\").replace(/[?].*/, \"\")\n                ) {\n                    this.selectAttachment(attachment);\n                }\n            }\n        }\n        return attachments;\n    }\n\n    /**\n     * Utility method used by the MediaDialog component.\n     */\n    static async createElements(selectedMedia, { orm }) {\n        return Promise.all(\n            selectedMedia.map(async (attachment) => {\n                const linkEl = document.createElement(\"a\");\n                let href = `/web/content/${encodeURIComponent(\n                    attachment.id\n                )}?unique=${encodeURIComponent(attachment.checksum)}&download=true`;\n                if (!attachment.public) {\n                    let accessToken = attachment.access_token;\n                    if (!accessToken) {\n                        [accessToken] = await orm.call(\"ir.attachment\", \"generate_access_token\", [\n                            attachment.id,\n                        ]);\n                    }\n                    href += `&access_token=${encodeURIComponent(accessToken)}`;\n                }\n                linkEl.href = href;\n                linkEl.title = attachment.name;\n                linkEl.dataset.mimetype = attachment.mimetype;\n                return linkEl;\n            })\n        );\n    }\n}\n", "import { DocumentSelector } from \"@html_editor/main/media/media_dialog/document_selector\";\n\n/**\n * Override the @see DocumentSelector to manage files in a @see MediaDialog used\n * by the /file command. The purpose of this override is to merge images in the\n * documents tab of the MediaDialog, since the /file block displays a default\n * mimetype for every files.\n */\nexport class FileDocumentsSelector extends DocumentSelector {\n    /**\n     * @override\n     * Filter files for the documents tab of the MediaDialog. Any file with a\n     * mimetype is valid. (images and documents are displayed together)\n     *\n     * As KnowledgeDocumentsSelector is an aggregate of multiple kinds of\n     * files, images included, the domain should be adjusted with the same\n     * constraints as @see image_selector.js\n     */\n    get attachmentsDomain() {\n        const domain = super.attachmentsDomain.map((d) => {\n            if (d[0] === \"mimetype\") {\n                return [\"mimetype\", \"!=\", false];\n            }\n            return d;\n        });\n        domain.unshift(\n            \"&\",\n            \"|\",\n            [\"url\", \"=\", null],\n            \"&\",\n            \"!\",\n            [\"url\", \"=like\", \"/%/static/%\"],\n            \"!\",\n            \"|\",\n            [\"url\", \"=ilike\", \"/html_editor/shape/%\"],\n            [\"url\", \"=ilike\", \"/web_editor/shape/%\"],\n        );\n        domain.push(\"!\", [\"name\", \"=like\", \"%.crop\"]);\n        return domain;\n    }\n}\n", "import { renderToElement } from \"@web/core/utils/render\";\nimport { MediaDialog, TABS } from \"@html_editor/main/media/media_dialog/media_dialog\";\nimport { FileDocumentsSelector } from \"./file_documents_selector\";\n\n/**\n * FileMediaDialog will allow to select documents and images altogether\n * for the /file command.\n */\nexport class FileMediaDialog extends MediaDialog {\n    /**\n     * @override\n     */\n    addTabs() {\n        super.addTabs(...arguments);\n        this.addTab({\n            ...TABS.DOCUMENTS,\n            id: \"MIXED_FILES\",\n            Component: FileDocumentsSelector,\n        });\n    }\n    /**\n     * @override\n     * Render the selected media. This needs a custom implementation because\n     * the media is rendered as a Behavior blueprint for Knowledge, hence\n     * no super call.\n     *\n     * @param {Object} selectedMedia First element of the selectedMediaArray,\n     *                 which has length = 1 in this case because this component\n     *                 is meant to be used with the prop `multiSelect = false`\n     * @returns {Array<HTMLElement>}\n     */\n    async renderMedia([selectedMedia]) {\n        let accessToken = selectedMedia.access_token;\n        if (!selectedMedia.public || !accessToken) {\n            // Generate an access token so that anyone with read access to the\n            // article can view its files.\n            [accessToken] = await this.orm.call(\"ir.attachment\", \"generate_access_token\", [\n                selectedMedia.id,\n            ]);\n        }\n        const dotSplit = selectedMedia.name.split(\".\");\n        const extension = dotSplit.length > 1 ? dotSplit.pop() : undefined;\n        const fileData = {\n            access_token: accessToken,\n            checksum: selectedMedia.checksum,\n            extension,\n            filename: selectedMedia.name,\n            id: selectedMedia.id,\n            mimetype: selectedMedia.mimetype,\n            name: selectedMedia.name,\n            type: selectedMedia.type,\n            url: selectedMedia.url || \"\",\n        };\n        const fileBlock = renderToElement(\"html_editor.EmbeddedFileBlueprint\", {\n            embeddedProps: JSON.stringify({\n                fileData,\n            }),\n        });\n        return [fileBlock];\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { SearchMedia } from \"./search_media\";\n\nimport { Component, xml, useState, useRef, onWillStart, useEffect } from \"@odoo/owl\";\n\nexport const IMAGE_MIMETYPES = [\n    \"image/jpg\",\n    \"image/jpeg\",\n    \"image/jpe\",\n    \"image/png\",\n    \"image/svg+xml\",\n    \"image/gif\",\n    \"image/webp\",\n];\nexport const IMAGE_EXTENSIONS = [\".jpg\", \".jpeg\", \".jpe\", \".png\", \".svg\", \".gif\", \".webp\"];\n\nclass RemoveButton extends Component {\n    static template = xml`<i class=\"fa fa-trash o_existing_attachment_remove position-absolute top-0 end-0 p-2 bg-white-25 cursor-pointer opacity-0 opacity-100-hover z-index-1 transition-base\" t-att-title=\"removeTitle\" role=\"img\" t-att-aria-label=\"removeTitle\" t-on-click=\"this.remove\"/>`;\n    static props = [\"model?\", \"remove\"];\n    setup() {\n        this.removeTitle = _t(\"This file is attached to the current record.\");\n        if (this.props.model === \"ir.ui.view\") {\n            this.removeTitle = _t(\"This file is a public view attachment.\");\n        }\n    }\n\n    remove(ev) {\n        ev.stopPropagation();\n        this.props.remove();\n    }\n}\n\nexport class AttachmentError extends Component {\n    static components = { Dialog };\n    static template = xml`\n        <Dialog title=\"title\">\n            <div class=\"form-text\">\n                <p>The image could not be deleted because it is used in the\n                    following pages or views:</p>\n                <ul t-foreach=\"props.views\"  t-as=\"view\" t-key=\"view.id\">\n                    <li>\n                        <a t-att-href=\"'/odoo/ir.ui.view/' + window.encodeURIComponent(view.id)\">\n                            <t t-esc=\"view.name\"/>\n                        </a>\n                    </li>\n                </ul>\n            </div>\n            <t t-set-slot=\"footer\">\n                <button class=\"btn btn-primary\" t-on-click=\"() => this.props.close()\">\n                    Ok\n                </button>\n            </t>\n        </Dialog>`;\n    static props = [\"views\", \"close\"];\n    setup() {\n        this.title = _t(\"Alert\");\n    }\n}\n\nexport class Attachment extends Component {\n    static template = \"\";\n    static components = {\n        RemoveButton,\n    };\n    static props = [\"*\"];\n    setup() {\n        this.dialogs = useService(\"dialog\");\n    }\n\n    remove() {\n        this.dialogs.add(ConfirmationDialog, {\n            body: _t(\"Are you sure you want to delete this file?\"),\n            confirm: async () => {\n                const prevented = await rpc(\"/web_editor/attachment/remove\", {\n                    ids: [this.props.id],\n                });\n                if (!Object.keys(prevented).length) {\n                    this.props.onRemoved(this.props.id);\n                } else {\n                    this.dialogs.add(AttachmentError, {\n                        views: prevented[this.props.id],\n                    });\n                }\n            },\n        });\n    }\n}\n\nexport class FileSelectorControlPanel extends Component {\n    static template = \"html_editor.FileSelectorControlPanel\";\n    static components = {\n        SearchMedia,\n    };\n    static props = {\n        uploadUrl: Function,\n        validateUrl: Function,\n        uploadFiles: Function,\n        changeSearchService: Function,\n        changeShowOptimized: Function,\n        search: Function,\n        accept: { type: String, optional: true },\n        addText: { type: String, optional: true },\n        multiSelect: { type: true, optional: true },\n        needle: { type: String, optional: true },\n        searchPlaceholder: { type: String, optional: true },\n        searchService: { type: String, optional: true },\n        showOptimized: { type: Boolean, optional: true },\n        showOptimizedOption: { type: String, optional: true },\n        uploadText: { type: String, optional: true },\n        urlPlaceholder: { type: String, optional: true },\n        urlWarningTitle: { type: String, optional: true },\n        useMediaLibrary: { type: Boolean, optional: true },\n        useUnsplash: { type: Boolean, optional: true },\n    };\n    setup() {\n        this.state = useState({\n            showUrlInput: false,\n            urlInput: \"\",\n            isValidUrl: false,\n            isValidFileFormat: false,\n            isValidatingUrl: false,\n        });\n        this.debouncedValidateUrl = useDebounced(this.props.validateUrl, 500);\n\n        this.fileInput = useRef(\"file-input\");\n    }\n\n    get showSearchServiceSelect() {\n        return this.props.searchService && this.props.needle;\n    }\n\n    get enableUrlUploadClick() {\n        return (\n            !this.state.showUrlInput ||\n            (this.state.urlInput && this.state.isValidUrl && this.state.isValidFileFormat)\n        );\n    }\n\n    async onUrlUploadClick() {\n        if (!this.state.showUrlInput) {\n            this.state.showUrlInput = true;\n        } else {\n            await this.props.uploadUrl(this.state.urlInput);\n            this.state.urlInput = \"\";\n        }\n    }\n\n    async onUrlInput(ev) {\n        this.state.isValidatingUrl = true;\n        const { isValidUrl, isValidFileFormat } = await this.debouncedValidateUrl(ev.target.value);\n        this.state.isValidFileFormat = isValidFileFormat;\n        this.state.isValidUrl = isValidUrl;\n        this.state.isValidatingUrl = false;\n    }\n\n    onClickUpload() {\n        this.fileInput.el.click();\n    }\n\n    async onChangeFileInput() {\n        const inputFiles = this.fileInput.el.files;\n        if (!inputFiles.length) {\n            return;\n        }\n        await this.props.uploadFiles(inputFiles);\n        this.fileInput.el.value = \"\";\n    }\n}\n\nexport class FileSelector extends Component {\n    static template = \"html_editor.FileSelector\";\n    static components = {\n        FileSelectorControlPanel,\n    };\n    static props = [\"*\"];\n\n    setup() {\n        this.notificationService = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.uploadService = useService(\"upload\");\n        this.keepLast = new KeepLast();\n\n        this.loadMoreButtonRef = useRef(\"load-more-button\");\n        this.existingAttachmentsRef = useRef(\"existing-attachments\");\n\n        this.state = useState({\n            attachments: [],\n            canScrollAttachments: false,\n            canLoadMoreAttachments: false,\n            isFetchingAttachments: false,\n            needle: \"\",\n        });\n\n        this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY = 30;\n\n        onWillStart(async () => {\n            this.state.attachments = await this.fetchAttachments(\n                this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY,\n                0\n            );\n        });\n\n        this.debouncedOnScroll = useDebounced(this.updateScroll, 15);\n        this.debouncedScrollUpdate = useDebounced(this.updateScroll, 500);\n\n        useEffect(\n            (modalEl) => {\n                if (modalEl) {\n                    modalEl.addEventListener(\"scroll\", this.debouncedOnScroll);\n                    return () => {\n                        modalEl.removeEventListener(\"scroll\", this.debouncedOnScroll);\n                    };\n                }\n            },\n            () => [this.props.modalRef.el?.querySelector(\"main.modal-body\")]\n        );\n\n        useEffect(\n            () => {\n                // Updating the scroll button each time the attachments change.\n                // Hiding the \"Load more\" button to prevent it from flickering.\n                this.loadMoreButtonRef.el.classList.add(\"o_hide_loading\");\n                this.state.canScrollAttachments = false;\n                this.debouncedScrollUpdate();\n            },\n            () => [this.allAttachments.length]\n        );\n    }\n\n    get canLoadMore() {\n        return this.state.canLoadMoreAttachments;\n    }\n\n    get hasContent() {\n        return this.state.attachments.length;\n    }\n\n    get isFetching() {\n        return this.state.isFetchingAttachments;\n    }\n\n    get selectedAttachmentIds() {\n        return this.props.selectedMedia[this.props.id]\n            .filter((media) => media.mediaType === \"attachment\")\n            .map(({ id }) => id);\n    }\n\n    get attachmentsDomain() {\n        const domain = [\n            \"&\",\n            [\"res_model\", \"=\", this.props.resModel],\n            [\"res_id\", \"=\", this.props.resId || 0],\n        ];\n        domain.unshift(\"|\", [\"public\", \"=\", true]);\n        domain.push([\"name\", \"ilike\", this.state.needle]);\n        return domain;\n    }\n\n    get allAttachments() {\n        return this.state.attachments;\n    }\n\n    validateUrl(url) {\n        const path = url.split(\"?\")[0];\n        const isValidUrl = /^.+\\..+$/.test(path); // TODO improve\n        const isValidFileFormat = true;\n        return { isValidUrl, isValidFileFormat, path };\n    }\n\n    async fetchAttachments(limit, offset) {\n        this.state.isFetchingAttachments = true;\n        let attachments = [];\n        try {\n            attachments = await this.orm.call(\"ir.attachment\", \"search_read\", [], {\n                domain: this.attachmentsDomain,\n                fields: [\n                    \"name\",\n                    \"mimetype\",\n                    \"description\",\n                    \"checksum\",\n                    \"url\",\n                    \"type\",\n                    \"res_id\",\n                    \"res_model\",\n                    \"public\",\n                    \"access_token\",\n                    \"image_src\",\n                    \"image_width\",\n                    \"image_height\",\n                    \"original_id\",\n                ],\n                order: \"id desc\",\n                // Try to fetch first record of next page just to know whether there is a next page.\n                limit,\n                offset,\n            });\n            attachments.forEach((attachment) => (attachment.mediaType = \"attachment\"));\n        } catch (e) {\n            // Reading attachments as a portal user is not permitted and will raise\n            // an access error so we catch the error silently and don't return any\n            // attachment so he can still use the wizard and upload an attachment\n            if (e.exceptionName !== \"odoo.exceptions.AccessError\") {\n                throw e;\n            }\n        }\n        this.state.canLoadMoreAttachments =\n            attachments.length >= this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY;\n        this.state.isFetchingAttachments = false;\n        return attachments;\n    }\n\n    async handleLoadMore() {\n        await this.loadMore();\n    }\n\n    async loadMore() {\n        return this.keepLast\n            .add(\n                this.fetchAttachments(\n                    this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY,\n                    this.state.attachments.length\n                )\n            )\n            .then((newAttachments) => {\n                // This is never reached if another search or loadMore occurred.\n                this.state.attachments.push(...newAttachments);\n            });\n    }\n\n    async handleSearch(needle) {\n        await this.search(needle);\n    }\n\n    async search(needle) {\n        // Prepare in case loadMore results are obtained instead.\n        this.state.attachments = [];\n        // Fetch attachments relies on the state's needle.\n        this.state.needle = needle;\n        return this.keepLast\n            .add(this.fetchAttachments(this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY, 0))\n            .then((attachments) => {\n                // This is never reached if a new search occurred.\n                this.state.attachments = attachments;\n            });\n    }\n\n    async uploadFiles(files) {\n        await this.uploadService.uploadFiles(\n            files,\n            { resModel: this.props.resModel, resId: this.props.resId },\n            (attachment) => this.onUploaded(attachment)\n        );\n    }\n\n    async uploadUrl(url) {\n        await fetch(url)\n            .then(async (result) => {\n                const blob = await result.blob();\n                blob.id = new Date().getTime();\n                blob.name = new URL(url).pathname.split(\"/\").findLast((s) => s);\n                await this.uploadFiles([blob]);\n            })\n            .catch(async () => {\n                await new Promise((resolve) => {\n                    // If it works from an image, use URL.\n                    const imageEl = document.createElement(\"img\");\n                    imageEl.onerror = () => {\n                        // This message is about the blob fetch failure.\n                        // It is only displayed if the fallback did not work.\n                        this.notificationService.add(\n                            _t(\"An error occurred while fetching the entered URL.\"),\n                            {\n                                title: _t(\"Error\"),\n                                sticky: true,\n                            }\n                        );\n                        resolve();\n                    };\n                    imageEl.onload = () => {\n                        this.uploadService\n                            .uploadUrl(\n                                url,\n                                {\n                                    resModel: this.props.resModel,\n                                    resId: this.props.resId,\n                                },\n                                (attachment) => this.onUploaded(attachment)\n                            )\n                            .then(resolve);\n                    };\n                    imageEl.src = url;\n                });\n            });\n    }\n\n    async onUploaded(attachment) {\n        this.state.attachments = [\n            attachment,\n            ...this.state.attachments.filter((attach) => attach.id !== attachment.id),\n        ];\n        this.selectAttachment(attachment);\n        if (!this.props.multiSelect) {\n            await this.props.save();\n        }\n        if (this.props.onAttachmentChange) {\n            this.props.onAttachmentChange(attachment);\n        }\n    }\n\n    onRemoved(attachmentId) {\n        this.state.attachments = this.state.attachments.filter(\n            (attachment) => attachment.id !== attachmentId\n        );\n    }\n\n    selectAttachment(attachment) {\n        this.props.selectMedia({ ...attachment, mediaType: \"attachment\" });\n    }\n\n    selectInitialMedia() {\n        return (\n            this.props.media &&\n            this.constructor.tagNames.includes(this.props.media.tagName) &&\n            !this.selectedAttachmentIds.length\n        );\n    }\n\n    /**\n     * Updates the scroll button, depending on whether the \"Load more\" button is\n     * fully visible or not.\n     */\n    updateScroll() {\n        const loadMoreTop = this.loadMoreButtonRef.el.getBoundingClientRect().top;\n        const modalEl = this.props.modalRef.el.querySelector(\"main.modal-body\");\n        const modalBottom = modalEl.getBoundingClientRect().bottom;\n        this.state.canScrollAttachments = loadMoreTop >= modalBottom;\n        this.loadMoreButtonRef.el.classList.remove(\"o_hide_loading\");\n    }\n\n    /**\n     * Checks if the attachment is (partially) hidden.\n     *\n     * @param {Element} attachmentEl the attachment \"container\"\n     * @returns {Boolean} true if the attachment is hidden, false otherwise.\n     */\n    isAttachmentHidden(attachmentEl) {\n        const attachmentBottom = Math.round(attachmentEl.getBoundingClientRect().bottom);\n        const modalEl = this.props.modalRef.el.querySelector(\"main.modal-body\");\n        const modalBottom = modalEl.getBoundingClientRect().bottom;\n        return attachmentBottom > modalBottom;\n    }\n\n    /**\n     * Scrolls two attachments rows at a time. If there are not enough rows,\n     * scrolls to the \"Load more\" button.\n     */\n    handleScrollAttachments() {\n        let scrollToEl = this.loadMoreButtonRef.el;\n        const attachmentEls = [\n            ...this.existingAttachmentsRef.el.querySelectorAll(\".o_existing_attachment_cell\"),\n        ];\n        const firstHiddenAttachmentEl = attachmentEls.find((el) => this.isAttachmentHidden(el));\n        if (firstHiddenAttachmentEl) {\n            const attachmentBottom = firstHiddenAttachmentEl.getBoundingClientRect().bottom;\n            const attachmentIndex = attachmentEls.indexOf(firstHiddenAttachmentEl);\n            const firstNextRowAttachmentEl = attachmentEls.slice(attachmentIndex).find((el) => {\n                return el.getBoundingClientRect().bottom > attachmentBottom;\n            });\n            scrollToEl = firstNextRowAttachmentEl || scrollToEl;\n        }\n        scrollToEl.scrollIntoView({ block: \"end\", inline: \"nearest\", behavior: \"smooth\" });\n    }\n}\n", "import { SearchMedia } from \"./search_media\";\nimport { fonts } from \"@html_editor/utils/fonts\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class IconSelector extends Component {\n    static mediaSpecificClasses = [\"fa\"];\n    static mediaSpecificStyles = [\"color\", \"background-color\"];\n    static mediaExtraClasses = [\n        \"rounded-circle\",\n        \"rounded\",\n        \"img-thumbnail\",\n        \"shadow\",\n        /^text-\\S+$/,\n        /^bg-\\S+$/,\n        /^fa-\\S+$/,\n    ];\n    static tagNames = [\"SPAN\", \"I\"];\n    static template = \"html_editor.IconSelector\";\n    static components = {\n        SearchMedia,\n    };\n    static props = [\"*\"];\n\n    setup() {\n        this.state = useState({\n            fonts: this.props.fonts,\n            needle: \"\",\n        });\n    }\n\n    get selectedMediaIds() {\n        return this.props.selectedMedia[this.props.id].map(({ id }) => id);\n    }\n\n    search(needle) {\n        this.state.needle = needle;\n        if (!this.state.needle) {\n            this.state.fonts = this.props.fonts;\n        } else {\n            this.state.fonts = this.props.fonts.map((font) => {\n                const icons = font.icons.filter(\n                    (icon) => icon.alias.indexOf(this.state.needle) >= 0\n                );\n                return { ...font, icons };\n            });\n        }\n    }\n\n    async onClickIcon(font, icon) {\n        this.props.selectMedia({\n            ...icon,\n            fontBase: font.base,\n            // To check if the icon has changed, we only need to compare\n            // an alias of the icon with the class from the old media (some\n            // icons can have multiple classes e.g. \"fa-gears\" ~ \"fa-cogs\")\n            initialIconChanged:\n                this.props.media &&\n                !icon.names.some((name) => this.props.media.classList.contains(name)),\n        });\n        await this.props.save();\n    }\n\n    /**\n     * Utility methods, used by the MediaDialog component.\n     */\n    static createElements(selectedMedia) {\n        return selectedMedia.map((icon) => {\n            const iconEl = document.createElement(\"span\");\n            iconEl.classList.add(icon.fontBase, icon.names[0]);\n            return iconEl;\n        });\n    }\n    static initFonts() {\n        fonts.computeFonts();\n        const allFonts = fonts.fontIcons.map(({ cssData, base }) => {\n            const uniqueIcons = Array.from(\n                new Map(\n                    cssData.map((icon) => {\n                        const alias = icon.names.join(\",\");\n                        const id = `${base}_${alias}`;\n                        return [id, { ...icon, alias, id }];\n                    })\n                ).values()\n            );\n            return { base, icons: uniqueIcons };\n        });\n        return allFonts;\n    }\n}\n", "import { useEffect, useRef, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { DEFAULT_PALETTE } from \"@html_editor/utils/color\";\nimport { getCSSVariableValue, getHtmlStyle } from \"@html_editor/utils/formatting\";\nimport { Attachment, FileSelector, IMAGE_EXTENSIONS, IMAGE_MIMETYPES } from \"./file_selector\";\n\nexport class AutoResizeImage extends Attachment {\n    static template = \"html_editor.AutoResizeImage\";\n    setup() {\n        super.setup();\n\n        this.image = useRef(\"auto-resize-image\");\n        this.container = useRef(\"auto-resize-image-container\");\n\n        this.state = useState({\n            loaded: false,\n        });\n\n        useEffect(\n            () => {\n                this.image.el.addEventListener(\"load\", () => this.onImageLoaded());\n                return this.image.el.removeEventListener(\"load\", () => this.onImageLoaded());\n            },\n            () => []\n        );\n    }\n\n    async onImageLoaded() {\n        if (!this.image.el) {\n            // Do not fail if already removed.\n            return;\n        }\n        if (this.props.onLoaded) {\n            await this.props.onLoaded(this.image.el);\n            if (!this.image.el) {\n                // If replaced by colored version, aspect ratio will be\n                // computed on it instead.\n                return;\n            }\n        }\n        const aspectRatio = this.image.el.offsetWidth / this.image.el.offsetHeight;\n        const width = aspectRatio * this.props.minRowHeight;\n        this.container.el.style.flexGrow = width;\n        this.container.el.style.flexBasis = `${width}px`;\n        this.state.loaded = true;\n    }\n}\nconst newLocal = \"img-fluid\";\nexport class ImageSelector extends FileSelector {\n    static mediaSpecificClasses = [\"img\", newLocal, \"o_we_custom_image\"];\n    static mediaSpecificStyles = [];\n    static mediaExtraClasses = [\n        \"rounded-circle\",\n        \"rounded\",\n        \"img-thumbnail\",\n        \"shadow\",\n        \"w-25\",\n        \"w-50\",\n        \"w-75\",\n        \"w-100\",\n    ];\n    static tagNames = [\"IMG\"];\n    static attachmentsListTemplate = \"html_editor.ImagesListTemplate\";\n    static components = {\n        ...FileSelector.components,\n        AutoResizeImage,\n    };\n\n    setup() {\n        super.setup();\n\n        this.keepLastLibraryMedia = new KeepLast();\n\n        this.state.libraryMedia = [];\n        this.state.libraryResults = null;\n        this.state.isFetchingLibrary = false;\n        this.state.searchService = \"all\";\n        this.state.showOptimized = false;\n        this.NUMBER_OF_MEDIA_TO_DISPLAY = 10;\n\n        this.uploadText = _t(\"Upload an image\");\n        this.urlPlaceholder = \"https://www.odoo.com/logo.png\";\n        this.addText = _t(\"Add URL\");\n        this.searchPlaceholder = _t(\"Search an image\");\n        this.urlWarningTitle = _t(\n            \"Uploaded image's format is not supported. Try with: \" + IMAGE_EXTENSIONS.join(\", \")\n        );\n        this.allLoadedText = _t(\"All images have been loaded\");\n        this.showOptimizedOption = this.env.debug;\n        this.MIN_ROW_HEIGHT = 128;\n\n        this.fileMimetypes = IMAGE_MIMETYPES.join(\",\");\n    }\n\n    get canLoadMore() {\n        // The user can load more library media only when the filter is set.\n        if (this.state.searchService === \"media-library\") {\n            return (\n                this.state.libraryResults &&\n                this.state.libraryMedia.length < this.state.libraryResults\n            );\n        }\n        return super.canLoadMore;\n    }\n\n    get hasContent() {\n        if (this.state.searchService === \"all\") {\n            return super.hasContent || !!this.state.libraryMedia.length;\n        } else if (this.state.searchService === \"media-library\") {\n            return !!this.state.libraryMedia.length;\n        }\n        return super.hasContent;\n    }\n\n    get isFetching() {\n        return super.isFetching || this.state.isFetchingLibrary;\n    }\n\n    get selectedMediaIds() {\n        return this.props.selectedMedia[this.props.id]\n            .filter((media) => media.mediaType === \"libraryMedia\")\n            .map(({ id }) => id);\n    }\n\n    get allAttachments() {\n        return [...super.allAttachments, ...this.state.libraryMedia];\n    }\n\n    get attachmentsDomain() {\n        const domain = super.attachmentsDomain;\n        domain.push([\"mimetype\", \"in\", IMAGE_MIMETYPES]);\n        if (!this.props.useMediaLibrary) {\n            domain.push(\"|\", [\"url\", \"=\", false],\n                \"!\", \"|\", [\"url\", \"=ilike\", \"/html_editor/shape/%\"], [\"url\", \"=ilike\", \"/web_editor/shape/%\"],\n            );\n        }\n        domain.push(\"!\", [\"name\", \"=like\", \"%.crop\"]);\n        domain.push(\"|\", [\"type\", \"=\", \"binary\"], \"!\", [\"url\", \"=like\", \"/%/static/%\"]);\n\n        // Optimized images (meaning they are related to an `original_id`) can\n        // only be shown in debug mode as the toggler to make those images\n        // appear is hidden when not in debug mode.\n        // There is thus no point to fetch those optimized images outside debug\n        // mode. Worst, it leads to bugs: it might fetch only optimized images\n        // when clicking on \"load more\" which will look like it's bugged as no\n        // images will appear on screen (they all will be hidden).\n        if (!this.env.debug) {\n            const subDomain = [false];\n\n            // Particular exception: if the edited image is an optimized\n            // image, we need to fetch it too so it's displayed as the\n            // selected image when opening the media dialog.\n            // We might get a few more optimized image than necessary if the\n            // original image has multiple optimized images but it's not a\n            // big deal.\n            const originalId = this.props.media && this.props.media.dataset.originalId;\n            if (originalId) {\n                subDomain.push(originalId);\n            }\n\n            domain.push([\"original_id\", \"in\", subDomain]);\n        }\n\n        return domain;\n    }\n\n    async uploadFiles(files) {\n        await this.uploadService.uploadFiles(\n            files,\n            { resModel: this.props.resModel, resId: this.props.resId, isImage: true },\n            (attachment) => this.onUploaded(attachment)\n        );\n    }\n\n    async validateUrl(...args) {\n        const { isValidUrl, path } = super.validateUrl(...args);\n        const isValidFileFormat =\n            isValidUrl &&\n            (await new Promise((resolve) => {\n                const img = new Image();\n                img.src = path;\n                img.onload = () => resolve(true);\n                img.onerror = () => resolve(false);\n            }));\n        return { isValidFileFormat, isValidUrl };\n    }\n\n    isInitialMedia(attachment) {\n        if (this.props.media.dataset.originalSrc) {\n            return this.props.media.dataset.originalSrc === attachment.image_src;\n        }\n        return this.props.media.getAttribute(\"src\") === attachment.image_src;\n    }\n\n    async fetchAttachments(limit, offset) {\n        const attachments = await super.fetchAttachments(limit, offset);\n        // Color-substitution for dynamic SVG attachment\n        const primaryColors = {};\n        const htmlStyle = getHtmlStyle(document);\n        for (let color = 1; color <= 5; color++) {\n            primaryColors[color] = getCSSVariableValue(\"o-color-\" + color, htmlStyle);\n        }\n        return attachments.map((attachment) => {\n            if (attachment.image_src.startsWith(\"/\")) {\n                const newURL = new URL(attachment.image_src, window.location.origin);\n                // Set the main colors of dynamic SVGs to o-color-1~5\n                if (\n                    attachment.image_src.startsWith(\"/html_editor/shape/\") ||\n                    attachment.image_src.startsWith(\"/web_editor/shape/\")\n                ) {\n                    newURL.searchParams.forEach((value, key) => {\n                        const match = key.match(/^c([1-5])$/);\n                        if (match) {\n                            newURL.searchParams.set(key, primaryColors[match[1]]);\n                        }\n                    });\n                } else {\n                    // Set height so that db images load faster\n                    newURL.searchParams.set(\"height\", 2 * this.MIN_ROW_HEIGHT);\n                }\n                attachment.thumbnail_src = newURL.pathname + newURL.search;\n            }\n            if (this.selectInitialMedia() && this.isInitialMedia(attachment)) {\n                this.selectAttachment(attachment);\n            }\n            return attachment;\n        });\n    }\n\n    async fetchLibraryMedia(offset) {\n        if (!this.state.needle) {\n            return { media: [], results: null };\n        }\n\n        this.state.isFetchingLibrary = true;\n        try {\n            const response = await rpc(\n                \"/web_editor/media_library_search\",\n                {\n                    query: this.state.needle,\n                    offset: offset,\n                },\n                {\n                    silent: true,\n                }\n            );\n            this.state.isFetchingLibrary = false;\n            const media = (response.media || []).slice(0, this.NUMBER_OF_MEDIA_TO_DISPLAY);\n            media.forEach((record) => (record.mediaType = \"libraryMedia\"));\n            return { media, results: response.results };\n        } catch {\n            // Either API endpoint doesn't exist or is misconfigured.\n            console.error(`Couldn't reach API endpoint.`);\n            this.state.isFetchingLibrary = false;\n            return { media: [], results: null };\n        }\n    }\n\n    async loadMore(...args) {\n        await super.loadMore(...args);\n        if (\n            !this.props.useMediaLibrary ||\n            // The user can load more library media only when the filter is set.\n            this.state.searchService !== \"media-library\"\n        ) {\n            return;\n        }\n        return this.keepLastLibraryMedia\n            .add(this.fetchLibraryMedia(this.state.libraryMedia.length))\n            .then(({ media }) => {\n                // This is never reached if another search or loadMore occurred.\n                this.state.libraryMedia.push(...media);\n            });\n    }\n\n    async search(...args) {\n        await super.search(...args);\n        if (!this.props.useMediaLibrary) {\n            return;\n        }\n        if (!this.state.needle) {\n            this.state.searchService = \"all\";\n        }\n        this.state.libraryMedia = [];\n        this.state.libraryResults = 0;\n        return this.keepLastLibraryMedia\n            .add(this.fetchLibraryMedia(0))\n            .then(({ media, results }) => {\n                // This is never reached if a new search occurred.\n                this.state.libraryMedia = media;\n                this.state.libraryResults = results;\n            });\n    }\n\n    async onClickAttachment(attachment) {\n        this.selectAttachment(attachment);\n        if (!this.props.multiSelect) {\n            await this.props.save();\n        }\n    }\n\n    async onClickMedia(media) {\n        this.props.selectMedia({ ...media, mediaType: \"libraryMedia\" });\n        if (!this.props.multiSelect) {\n            await this.props.save();\n        }\n    }\n\n    /**\n     * Utility method used by the MediaDialog component.\n     */\n    static async createElements(selectedMedia, { orm }) {\n        // Create all media-library attachments.\n        const toSave = Object.fromEntries(\n            selectedMedia\n                .filter((media) => media.mediaType === \"libraryMedia\")\n                .map((media) => [\n                    media.id,\n                    {\n                        query: media.query || \"\",\n                        is_dynamic_svg: !!media.isDynamicSVG,\n                        dynamic_colors: media.dynamicColors,\n                    },\n                ])\n        );\n        let savedMedia = [];\n        if (Object.keys(toSave).length !== 0) {\n            savedMedia = await rpc(\"/html_editor/save_library_media\", { media: toSave });\n        }\n        const selected = selectedMedia\n            .filter((media) => media.mediaType === \"attachment\")\n            .concat(savedMedia)\n            .map((attachment) => {\n                // Color-customize dynamic SVGs with the theme colors\n                if (attachment.image_src && (\n                    attachment.image_src.startsWith(\"/html_editor/shape/\") ||\n                    attachment.image_src.startsWith(\"/web_editor/shape/\")\n                )) {\n                    const colorCustomizedURL = new URL(\n                        attachment.image_src,\n                        window.location.origin\n                    );\n                    const htmlStyle = getHtmlStyle(document);\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(`o-color-${match[1]}`, htmlStyle)\n                            );\n                        }\n                    });\n                    attachment.image_src = colorCustomizedURL.pathname + colorCustomizedURL.search;\n                }\n                return attachment;\n            });\n        return Promise.all(\n            selected.map(async (attachment) => {\n                const imageEl = document.createElement(\"img\");\n                let src = attachment.image_src;\n                if (!attachment.public && !attachment.url) {\n                    let accessToken = attachment.access_token;\n                    if (!accessToken) {\n                        [accessToken] = await orm.call(\"ir.attachment\", \"generate_access_token\", [\n                            attachment.id,\n                        ]);\n                    }\n                    src += `?access_token=${encodeURIComponent(accessToken)}`;\n                }\n                imageEl.src = src;\n                imageEl.alt = attachment.description || \"\";\n                return imageEl;\n            })\n        );\n    }\n\n    async onImageLoaded(imgEl, attachment) {\n        this.debouncedScrollUpdate();\n        if (attachment.mediaType === \"libraryMedia\" && !imgEl.src.startsWith(\"blob\")) {\n            // This call applies the theme's color palette to the\n            // loaded illustration. Upon replacement of the image,\n            // `onImageLoad` is called again, but the replacement image\n            // has an URL that starts with 'blob'. The condition above\n            // uses this to avoid an infinite loop.\n            await this.onLibraryImageLoaded(imgEl, attachment);\n        }\n    }\n\n    /**\n     * This converts the colors of an svg coming from the media library to\n     * the palette's ones, and make them dynamic.\n     *\n     * @param {HTMLElement} imgEl\n     * @param {Object} media\n     * @returns\n     */\n    async onLibraryImageLoaded(imgEl, media) {\n        const mediaUrl = imgEl.src;\n        try {\n            const response = await fetch(mediaUrl);\n            if (response.headers.get(\"content-type\") === \"image/svg+xml\") {\n                let svg = await response.text();\n                const dynamicColors = {};\n                const combinedColorsRegex = new RegExp(\n                    Object.values(DEFAULT_PALETTE).join(\"|\"),\n                    \"gi\"\n                );\n                const htmlStyle = getHtmlStyle(document);\n                svg = svg.replace(combinedColorsRegex, (match) => {\n                    const colorId = Object.keys(DEFAULT_PALETTE).find(\n                        (key) => DEFAULT_PALETTE[key] === match.toUpperCase()\n                    );\n                    const colorKey = \"c\" + colorId;\n                    dynamicColors[colorKey] = getCSSVariableValue(\"o-color-\" + colorId, htmlStyle);\n                    return dynamicColors[colorKey];\n                });\n                const fileName = mediaUrl.split(\"/\").pop();\n                const file = new File([svg], fileName, {\n                    type: \"image/svg+xml\",\n                });\n                imgEl.src = URL.createObjectURL(file);\n                if (Object.keys(dynamicColors).length) {\n                    media.isDynamicSVG = true;\n                    media.dynamicColors = dynamicColors;\n                }\n            }\n        } catch {\n            console.error(\n                \"CORS is misconfigured on the API server, image will be treated as non-dynamic.\"\n            );\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useService, useChildRef } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Notebook } from \"@web/core/notebook/notebook\";\nimport { ImageSelector } from \"./image_selector\";\nimport { DocumentSelector } from \"./document_selector\";\nimport { IconSelector } from \"./icon_selector\";\nimport { VideoSelector } from \"./video_selector\";\n\nimport { Component, useState, useRef, useEffect } from \"@odoo/owl\";\n\nexport const TABS = {\n    IMAGES: {\n        id: \"IMAGES\",\n        title: _t(\"Images\"),\n        Component: ImageSelector,\n    },\n    DOCUMENTS: {\n        id: \"DOCUMENTS\",\n        title: _t(\"Documents\"),\n        Component: DocumentSelector,\n    },\n    ICONS: {\n        id: \"ICONS\",\n        title: _t(\"Icons\"),\n        Component: IconSelector,\n    },\n    VIDEOS: {\n        id: \"VIDEOS\",\n        title: _t(\"Videos\"),\n        Component: VideoSelector,\n    },\n};\n\nexport class MediaDialog extends Component {\n    static template = \"html_editor.MediaDialog\";\n    static defaultProps = {\n        useMediaLibrary: true,\n    };\n    static components = {\n        ...Object.keys(TABS).map((key) => TABS[key].Component),\n        Dialog,\n        Notebook,\n    };\n    static props = [\"*\"];\n\n    setup() {\n        this.size = \"xl\";\n        this.contentClass = \"o_select_media_dialog h-100\";\n        this.title = _t(\"Select a media\");\n        this.modalRef = useChildRef();\n\n        this.orm = useService(\"orm\");\n        this.notificationService = useService(\"notification\");\n\n        this.tabs = [];\n        this.selectedMedia = useState({});\n\n        this.addButtonRef = useRef(\"add-button\");\n\n        this.initialIconClasses = [];\n\n        this.addTabs();\n        this.errorMessages = {};\n\n        this.state = useState({\n            activeTab: this.initialActiveTab,\n        });\n\n        useEffect(\n            (nbSelectedAttachments) => {\n                // Disable/enable the add button depending on whether some media\n                // are selected or not.\n                this.addButtonRef.el.toggleAttribute(\"disabled\", !nbSelectedAttachments);\n            },\n            () => [this.selectedMedia[this.state.activeTab].length]\n        );\n    }\n\n    get initialActiveTab() {\n        if (this.props.activeTab) {\n            return this.props.activeTab;\n        }\n        if (this.props.media) {\n            const correspondingTab = Object.keys(TABS).find((id) =>\n                TABS[id].Component.tagNames.includes(this.props.media.tagName)\n            );\n            if (correspondingTab) {\n                return correspondingTab;\n            }\n        }\n        return this.tabs[0].id;\n    }\n\n    addTab(tab, additionalProps = {}) {\n        this.selectedMedia[tab.id] = [];\n        this.tabs.push({\n            ...tab,\n            props: {\n                ...tab.props,\n                ...additionalProps,\n                id: tab.id,\n                resModel: this.props.resModel,\n                resId: this.props.resId,\n                media: this.props.media,\n                // multiImages: this.props.multiImages,\n                selectedMedia: this.selectedMedia,\n                selectMedia: (...args) =>\n                    this.selectMedia(...args, tab.id, additionalProps.multiSelect),\n                save: this.save.bind(this),\n                onAttachmentChange: this.props.onAttachmentChange,\n                errorMessages: (errorMessage) => (this.errorMessages[tab.id] = errorMessage),\n                modalRef: this.modalRef,\n            },\n        });\n    }\n\n    addTabs() {\n        const onlyImages =\n            this.props.onlyImages ||\n            (this.props.media &&\n                this.props.media.parentElement &&\n                (this.props.media.parentElement.dataset.oeField === \"image\" ||\n                    this.props.media.parentElement.dataset.oeType === \"image\"));\n        const noDocuments = onlyImages || this.props.noDocuments;\n        const noIcons = onlyImages || this.props.noIcons;\n        const noVideos = onlyImages || this.props.noVideos;\n\n        if (!this.props.noImages) {\n            this.addTab(TABS.IMAGES, {\n                useMediaLibrary: this.props.useMediaLibrary,\n                multiSelect: this.props.multiImages,\n            });\n        }\n        if (!noDocuments) {\n            this.addTab(TABS.DOCUMENTS);\n        }\n        if (!noIcons) {\n            const fonts = TABS.ICONS.Component.initFonts();\n            this.addTab(TABS.ICONS, {\n                fonts,\n            });\n\n            if (\n                this.props.media &&\n                TABS.ICONS.Component.tagNames.includes(this.props.media.tagName)\n            ) {\n                const classes = this.props.media.className.split(/\\s+/);\n                const mediaFont = fonts.find((font) => classes.includes(font.base));\n                if (mediaFont) {\n                    const selectedIcon = mediaFont.icons.find((icon) =>\n                        icon.names.some((name) => classes.includes(name))\n                    );\n                    if (selectedIcon) {\n                        this.initialIconClasses.push(...selectedIcon.names);\n                        this.selectMedia(selectedIcon, TABS.ICONS.id);\n                    }\n                }\n            }\n        }\n        if (!noVideos) {\n            this.addTab(TABS.VIDEOS, {\n                vimeoPreviewIds: this.props.vimeoPreviewIds,\n                isForBgVideo: this.props.isForBgVideo,\n            });\n        }\n    }\n\n    /**\n     * Render the selected media for insertion in the editor\n     *\n     * @param {Array<Object>} selectedMedia\n     * @returns {Array<HTMLElement>}\n     */\n    async renderMedia(selectedMedia) {\n        const elements = await TABS[this.state.activeTab].Component.createElements(selectedMedia, {\n            orm: this.orm,\n        });\n        elements.forEach((element) => {\n            if (this.props.media) {\n                element.classList.add(...this.props.media.classList);\n                const style = this.props.media.getAttribute(\"style\");\n                if (style) {\n                    element.setAttribute(\"style\", style);\n                }\n                if (this.state.activeTab === TABS.IMAGES.id) {\n                    if (this.props.media.dataset.shape) {\n                        element.dataset.shape = this.props.media.dataset.shape;\n                    }\n                    if (this.props.media.dataset.shapeColors) {\n                        element.dataset.shapeColors = this.props.media.dataset.shapeColors;\n                    }\n                    if (this.props.media.dataset.shapeFlip) {\n                        element.dataset.shapeFlip = this.props.media.dataset.shapeFlip;\n                    }\n                    if (this.props.media.dataset.shapeRotate) {\n                        element.dataset.shapeRotate = this.props.media.dataset.shapeRotate;\n                    }\n                    if (this.props.media.dataset.hoverEffect) {\n                        element.dataset.hoverEffect = this.props.media.dataset.hoverEffect;\n                    }\n                    if (this.props.media.dataset.hoverEffectColor) {\n                        element.dataset.hoverEffectColor =\n                            this.props.media.dataset.hoverEffectColor;\n                    }\n                    if (this.props.media.dataset.hoverEffectStrokeWidth) {\n                        element.dataset.hoverEffectStrokeWidth =\n                            this.props.media.dataset.hoverEffectStrokeWidth;\n                    }\n                    if (this.props.media.dataset.hoverEffectIntensity) {\n                        element.dataset.hoverEffectIntensity =\n                            this.props.media.dataset.hoverEffectIntensity;\n                    }\n                }\n            }\n            for (const otherTab of Object.keys(TABS).filter(\n                (key) => key !== this.state.activeTab\n            )) {\n                for (const property of TABS[otherTab].Component.mediaSpecificStyles) {\n                    element.style.removeProperty(property);\n                }\n                element.classList.remove(...TABS[otherTab].Component.mediaSpecificClasses);\n                const extraClassesToRemove = [];\n                for (const name of TABS[otherTab].Component.mediaExtraClasses) {\n                    if (typeof name === \"string\") {\n                        extraClassesToRemove.push(name);\n                    } else {\n                        // Regex\n                        for (const className of element.classList) {\n                            if (className.match(name)) {\n                                extraClassesToRemove.push(className);\n                            }\n                        }\n                    }\n                }\n                // Remove classes that do not also exist in the target type.\n                element.classList.remove(\n                    ...extraClassesToRemove.filter((candidateName) => {\n                        for (const name of TABS[this.state.activeTab].Component.mediaExtraClasses) {\n                            if (typeof name === \"string\") {\n                                if (candidateName === name) {\n                                    return false;\n                                }\n                            } else {\n                                // Regex\n                                for (const className of element.classList) {\n                                    if (className.match(candidateName)) {\n                                        return false;\n                                    }\n                                }\n                            }\n                        }\n                        return true;\n                    })\n                );\n            }\n            element.classList.remove(...this.initialIconClasses);\n            element.classList.remove(\"o_modified_image_to_save\");\n            element.classList.remove(\"oe_edited_link\");\n            element.classList.add(...TABS[this.state.activeTab].Component.mediaSpecificClasses);\n        });\n        return elements;\n    }\n\n    selectMedia(media, tabId, multiSelect) {\n        if (multiSelect) {\n            const isMediaSelected = this.selectedMedia[tabId]\n                .map(({ id }) => id)\n                .includes(media.id);\n            if (!isMediaSelected) {\n                this.selectedMedia[tabId].push(media);\n            } else {\n                this.selectedMedia[tabId] = this.selectedMedia[tabId].filter(\n                    (m) => m.id !== media.id\n                );\n            }\n        } else {\n            this.selectedMedia[tabId] = [media];\n        }\n    }\n\n    async save() {\n        if (this.errorMessages[this.state.activeTab]) {\n            this.notificationService.add(this.errorMessages[this.state.activeTab], {\n                type: \"danger\",\n            });\n            return;\n        }\n        const selectedMedia = this.selectedMedia[this.state.activeTab];\n        // TODO In master: clean the save method so it performs the specific\n        // adaptation before saving from the active media selector and find a\n        // way to simply close the dialog if the media element remains the same.\n        const saveSelectedMedia =\n            selectedMedia.length &&\n            (this.state.activeTab !== TABS.ICONS.id ||\n                selectedMedia[0].initialIconChanged ||\n                !this.props.media);\n        if (saveSelectedMedia) {\n            const elements = await this.renderMedia(selectedMedia);\n            if (this.props.multiImages) {\n                this.props.save(elements);\n            } else {\n                this.props.save(elements[0]);\n            }\n        }\n        this.props.close();\n    }\n\n    onTabChange(tab) {\n        this.state.activeTab = tab;\n    }\n}\n", "import { useDebounced } from \"@web/core/utils/timing\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\n\nimport { Component, xml, useEffect, useState } from \"@odoo/owl\";\n\nexport class SearchMedia extends Component {\n    static template = xml`\n        <div class=\"position-relative mw-lg-25 flex-grow-1 me-auto\">\n            <input type=\"text\" class=\"o_we_search o_input form-control\" t-att-placeholder=\"props.searchPlaceholder.trim()\" t-model=\"state.input\" t-ref=\"autofocus\"/>\n            <i class=\"oi oi-search input-group-text position-absolute end-0 top-50 me-n3 px-2 py-1 translate-middle bg-transparent border-0\" title=\"Search\" role=\"img\" aria-label=\"Search\"/>\n        </div>`;\n    static props = [\"searchPlaceholder\", \"search\", \"needle\"];\n    setup() {\n        useAutofocus({ mobile: true });\n        this.debouncedSearch = useDebounced(this.props.search, 1000);\n\n        this.state = useState({\n            input: this.props.needle || \"\",\n        });\n\n        useEffect(\n            (input) => {\n                // Do not trigger a search on the initial render.\n                if (this.hasRendered) {\n                    this.debouncedSearch(input);\n                } else {\n                    this.hasRendered = true;\n                }\n            },\n            () => [this.state.input]\n        );\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class ProgressBar extends Component {\n    static template = \"html_editor.ProgressBar\";\n    static props = {\n        progress: { type: Number, optional: true },\n        hasError: { type: Boolean, optional: true },\n        uploaded: { type: Boolean, optional: true },\n        name: String,\n        size: { type: String, optional: true },\n        errorMessage: { type: String, optional: true },\n    };\n    static defaultProps = {\n        progress: 0,\n        hasError: false,\n        uploaded: false,\n        size: \"\",\n        errorMessage: \"\",\n    };\n\n    get progress() {\n        return Math.round(this.props.progress);\n    }\n}\n\nexport class UploadProgressToast extends Component {\n    static template = \"html_editor.UploadProgressToast\";\n    static components = {\n        ProgressBar,\n    };\n    static props = {\n        close: Function,\n    };\n\n    setup() {\n        this.uploadService = useService(\"upload\");\n        this.state = useState(this.uploadService.progressToast);\n    }\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { UploadProgressToast } from \"./upload_progress_toast\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { checkFileSize } from \"@web/core/utils/files\";\nimport { humanNumber } from \"@web/core/utils/numbers\";\nimport { getDataURLFromFile } from \"@web/core/utils/urls\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { reactive } from \"@odoo/owl\";\n\nexport const AUTOCLOSE_DELAY = 3000;\nexport const AUTOCLOSE_DELAY_LONG = 8000;\n\nexport const uploadService = {\n    dependencies: [\"notification\"],\n    start(env, { notification }) {\n        let fileId = 0;\n        const progressToast = reactive({\n            files: {},\n            isVisible: false,\n        });\n\n        registry.category(\"main_components\").add(\"UploadProgressToast\", {\n            Component: UploadProgressToast,\n            props: {\n                close: () => (progressToast.isVisible = false),\n            },\n        });\n\n        const addFile = (file) => {\n            progressToast.files[file.id] = file;\n            progressToast.isVisible = true;\n            return progressToast.files[file.id];\n        };\n\n        const deleteFile = (fileId) => {\n            delete progressToast.files[fileId];\n            if (!Object.keys(progressToast.files).length) {\n                progressToast.isVisible = false;\n            }\n        };\n        return {\n            get progressToast() {\n                return progressToast;\n            },\n            get fileId() {\n                return fileId;\n            },\n            addFile,\n            deleteFile,\n            incrementId() {\n                fileId++;\n            },\n            uploadUrl: async (url, { resModel, resId }, onUploaded) => {\n                const attachment = await rpc(\"/html_editor/attachment/add_url\", {\n                    url,\n                    res_model: resModel,\n                    res_id: resId,\n                });\n                await onUploaded(attachment);\n            },\n            /**\n             * This takes an array of files (from an input HTMLElement), and\n             * uploads them while managing the UploadProgressToast.\n             *\n             * @param {Array<File>} files\n             * @param {Object} options\n             * @param {Function} onUploaded\n             */\n            uploadFiles: async (files, { resModel, resId, isImage }, onUploaded) => {\n                // Upload the smallest file first to block the user the least possible.\n                const sortedFiles = Array.from(files).sort((a, b) => a.size - b.size);\n                for (const file of sortedFiles) {\n                    let fileSize = file.size;\n                    if (!checkFileSize(fileSize, notification)) {\n                        return null;\n                    }\n                    if (!fileSize) {\n                        fileSize = \"\";\n                    } else {\n                        fileSize = humanNumber(fileSize) + \"B\";\n                    }\n\n                    const id = ++fileId;\n                    file.progressToastId = id;\n                    // This reactive object, built based on the files array,\n                    // is given as a prop to the UploadProgressToast.\n                    addFile({\n                        id,\n                        name: file.name,\n                        size: fileSize,\n                    });\n                }\n\n                // Upload one file at a time: no need to parallel as upload is\n                // limited by bandwidth.\n                for (const sortedFile of sortedFiles) {\n                    const file = progressToast.files[sortedFile.progressToastId];\n                    let dataURL;\n                    try {\n                        dataURL = await getDataURLFromFile(sortedFile);\n                    } catch {\n                        deleteFile(file.id);\n                        env.services.notification.add(\n                            sprintf(_t('Could not load the file \"%s\".'), sortedFile.name),\n                            { type: \"danger\" }\n                        );\n                        continue;\n                    }\n                    try {\n                        const xhr = new XMLHttpRequest();\n                        xhr.upload.addEventListener(\"progress\", (ev) => {\n                            const rpcComplete = (ev.loaded / ev.total) * 100;\n                            file.progress = rpcComplete;\n                        });\n                        xhr.upload.addEventListener(\"load\", function () {\n                            // Don't show yet success as backend code only starts now\n                            file.progress = 100;\n                        });\n                        const attachment = await rpc(\n                            \"/html_editor/attachment/add_data\",\n                            {\n                                name: file.name,\n                                data: dataURL.split(\",\")[1],\n                                res_id: resId,\n                                res_model: resModel,\n                                is_image: !!isImage,\n                                width: 0,\n                                quality: 0,\n                            },\n                            { xhr }\n                        );\n                        if (attachment.error) {\n                            file.hasError = true;\n                            file.errorMessage = attachment.error;\n                        } else {\n                            if (attachment.mimetype === \"image/webp\") {\n                                // Generate alternate format for reports.\n                                const image = document.createElement(\"img\");\n                                image.src = `data:image/webp;base64,${dataURL.split(\",\")[1]}`;\n                                await new Promise((resolve) =>\n                                    image.addEventListener(\"load\", resolve)\n                                );\n                                const canvas = document.createElement(\"canvas\");\n                                canvas.width = image.width;\n                                canvas.height = image.height;\n                                const ctx = canvas.getContext(\"2d\");\n                                ctx.fillStyle = \"rgb(255, 255, 255)\";\n                                ctx.fillRect(0, 0, canvas.width, canvas.height);\n                                ctx.drawImage(image, 0, 0);\n                                const altDataURL = canvas.toDataURL(\"image/jpeg\", 0.75);\n                                await rpc(\n                                    \"/html_editor/attachment/add_data\",\n                                    {\n                                        name: file.name.replace(/\\.webp$/, \".jpg\"),\n                                        data: altDataURL.split(\",\")[1],\n                                        res_id: attachment.id,\n                                        res_model: \"ir.attachment\",\n                                        is_image: true,\n                                        width: 0,\n                                        quality: 0,\n                                    },\n                                    { xhr }\n                                );\n                            }\n                            file.uploaded = true;\n                            await onUploaded(attachment);\n                        }\n                        // If there's an error, display the error message for longer\n                        const message_autoclose_delay = file.hasError\n                            ? AUTOCLOSE_DELAY_LONG\n                            : AUTOCLOSE_DELAY;\n                        setTimeout(() => deleteFile(file.id), message_autoclose_delay);\n                    } catch (error) {\n                        file.hasError = true;\n                        setTimeout(() => deleteFile(file.id), AUTOCLOSE_DELAY_LONG);\n                        throw error;\n                    }\n                }\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"upload\", uploadService);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { debounce } from \"@web/core/utils/timing\";\n\nimport { Component, useState, useRef, onMounted, onWillStart } from \"@odoo/owl\";\n\nclass VideoOption extends Component {\n    static template = \"html_editor.VideoOption\";\n    static props = {\n        description: { type: String, optional: true },\n        label: { type: String, optional: true },\n        onChangeOption: Function,\n        value: { type: Boolean, optional: true },\n    };\n}\n\nclass VideoIframe extends Component {\n    static template = \"html_editor.VideoIframe\";\n    static props = {\n        src: { type: String },\n    };\n}\n\nexport class VideoSelector extends Component {\n    static mediaSpecificClasses = [\"media_iframe_video\"];\n    static mediaSpecificStyles = [];\n    static mediaExtraClasses = [];\n    static tagNames = [\"IFRAME\", \"DIV\"];\n    static template = \"html_editor.VideoSelector\";\n    static components = {\n        VideoIframe,\n        VideoOption,\n    };\n    static props = {\n        selectMedia: Function,\n        errorMessages: Function,\n        vimeoPreviewIds: { type: Array, optional: true },\n        isForBgVideo: { type: Boolean, optional: true },\n        media: { type: Object, optional: true },\n        \"*\": true,\n    };\n    static defaultProps = {\n        vimeoPreviewIds: [],\n        isForBgVideo: false,\n    };\n\n    setup() {\n        this.http = useService(\"http\");\n\n        this.PLATFORMS = {\n            youtube: \"youtube\",\n            dailymotion: \"dailymotion\",\n            vimeo: \"vimeo\",\n            youku: \"youku\",\n        };\n\n        this.OPTIONS = {\n            autoplay: {\n                label: _t(\"Autoplay\"),\n                description: _t(\"Videos are muted when autoplay is enabled\"),\n                platforms: [\n                    this.PLATFORMS.youtube,\n                    this.PLATFORMS.dailymotion,\n                    this.PLATFORMS.vimeo,\n                ],\n                urlParameter: \"autoplay=1\",\n            },\n            loop: {\n                label: _t(\"Loop\"),\n                platforms: [this.PLATFORMS.youtube, this.PLATFORMS.vimeo],\n                urlParameter: \"loop=1\",\n            },\n            hide_controls: {\n                label: _t(\"Hide player controls\"),\n                platforms: [\n                    this.PLATFORMS.youtube,\n                    this.PLATFORMS.dailymotion,\n                    this.PLATFORMS.vimeo,\n                ],\n                urlParameter: \"controls=0\",\n            },\n            hide_fullscreen: {\n                label: _t(\"Hide fullscreen button\"),\n                platforms: [this.PLATFORMS.youtube],\n                urlParameter: \"fs=0\",\n                isHidden: () =>\n                    this.state.options.filter((option) => option.id === \"hide_controls\")[0].value,\n            },\n            hide_dm_logo: {\n                label: _t(\"Hide Dailymotion logo\"),\n                platforms: [this.PLATFORMS.dailymotion],\n                urlParameter: \"ui-logo=0\",\n            },\n            hide_dm_share: {\n                label: _t(\"Hide sharing button\"),\n                platforms: [this.PLATFORMS.dailymotion],\n                urlParameter: \"sharing-enable=0\",\n            },\n        };\n\n        this.state = useState({\n            options: [],\n            src: \"\",\n            urlInput: \"\",\n            platform: null,\n            vimeoPreviews: [],\n            errorMessage: \"\",\n        });\n        this.urlInputRef = useRef(\"url-input\");\n\n        onWillStart(async () => {\n            if (this.props.media) {\n                const src =\n                    this.props.media.dataset.oeExpression ||\n                    this.props.media.dataset.src ||\n                    (this.props.media.tagName === \"IFRAME\" &&\n                        this.props.media.getAttribute(\"src\")) ||\n                    \"\";\n                if (src) {\n                    this.state.urlInput = src;\n                    await this.updateVideo();\n\n                    this.state.options = this.state.options.map((option) => {\n                        const { urlParameter } = this.OPTIONS[option.id];\n                        return { ...option, value: src.indexOf(urlParameter) >= 0 };\n                    });\n                }\n            }\n        });\n\n        onMounted(async () => {\n            await Promise.all(\n                this.props.vimeoPreviewIds.map(async (videoId) => {\n                    const { thumbnail_url: thumbnailSrc } = await this.http.get(\n                        `https://vimeo.com/api/oembed.json?url=http%3A//vimeo.com/${encodeURIComponent(\n                            videoId\n                        )}`\n                    );\n                    this.state.vimeoPreviews.push({\n                        id: videoId,\n                        thumbnailSrc,\n                        src: `https://player.vimeo.com/video/${encodeURIComponent(videoId)}`,\n                    });\n                })\n            );\n        });\n\n        useAutofocus();\n\n        this.onChangeUrl = debounce((ev) => this.updateVideo(ev.target.value), 500);\n    }\n\n    get shownOptions() {\n        if (this.props.isForBgVideo) {\n            return [];\n        }\n        return this.state.options.filter(\n            (option) => !this.OPTIONS[option.id].isHidden || !this.OPTIONS[option.id].isHidden()\n        );\n    }\n\n    async onChangeOption(optionId) {\n        this.state.options = this.state.options.map((option) => {\n            if (option.id === optionId) {\n                return { ...option, value: !option.value };\n            }\n            return option;\n        });\n        await this.updateVideo();\n    }\n\n    async onClickSuggestion(src) {\n        this.state.urlInput = src;\n        await this.updateVideo();\n    }\n\n    async updateVideo() {\n        if (!this.state.urlInput) {\n            this.state.src = \"\";\n            this.state.urlInput = \"\";\n            this.state.options = [];\n            this.state.platform = null;\n            this.state.errorMessage = \"\";\n            /**\n             * When the url input is emptied, we need to call the `selectMedia`\n             * callback function to notify the other components that the media\n             * has changed.\n             */\n            this.props.selectMedia({});\n            return;\n        }\n\n        // Detect if we have an embed code rather than an URL\n        const embedMatch = this.state.urlInput.match(/(src|href)=[\"']?([^\"']+)?/);\n        if (embedMatch && embedMatch[2].length > 0 && embedMatch[2].indexOf(\"instagram\")) {\n            embedMatch[1] = embedMatch[2]; // Instagram embed code is different\n        }\n        const url = embedMatch ? embedMatch[1] : this.state.urlInput;\n\n        const options = {};\n        if (this.props.isForBgVideo) {\n            Object.keys(this.OPTIONS).forEach((key) => {\n                options[key] = true;\n            });\n        } else {\n            for (const option of this.shownOptions) {\n                options[option.id] = option.value;\n            }\n        }\n\n        const {\n            embed_url: src,\n            video_id: videoId,\n            params,\n            platform,\n        } = await this._getVideoURLData(url, options);\n\n        if (!src) {\n            this.state.errorMessage = _t(\"The provided url is not valid\");\n        } else if (!platform) {\n            this.state.errorMessage = _t(\"The provided url does not reference any supported video\");\n        } else {\n            this.state.errorMessage = \"\";\n        }\n        this.props.errorMessages(this.state.errorMessage);\n\n        const newOptions = [];\n        if (platform && platform !== this.state.platform) {\n            Object.keys(this.OPTIONS).forEach((key) => {\n                if (this.OPTIONS[key].platforms.includes(platform)) {\n                    const { label, description } = this.OPTIONS[key];\n                    newOptions.push({ id: key, label, description });\n                }\n            });\n        }\n\n        this.state.src = src;\n        this.props.selectMedia({\n            id: src,\n            src,\n            platform,\n            videoId,\n            params,\n        });\n        if (platform !== this.state.platform) {\n            this.state.platform = platform;\n            this.state.options = newOptions;\n        }\n    }\n\n    /**\n     * Keep rpc call in distinct method make it patchable by test.\n     */\n    async _getVideoURLData(url, options) {\n        return await rpc(\"/web_editor/video_url/data\", {\n            video_url: url,\n            ...options,\n        });\n    }\n\n    /**\n     * Utility method, called by the MediaDialog component.\n     */\n    static createElements(selectedMedia) {\n        return selectedMedia.map((video) => {\n            const div = document.createElement(\"div\");\n            div.dataset.oeExpression = video.src;\n            div.innerHTML =\n                '<div class=\"css_editable_mode_display\"></div>' +\n                '<div class=\"media_iframe_video_size\" contenteditable=\"false\"></div>' +\n                '<iframe frameborder=\"0\" contenteditable=\"false\" allowfullscreen=\"allowfullscreen\"></iframe>';\n\n            div.querySelector(\"iframe\").src = video.src;\n            return div;\n        });\n    }\n}\n", "import { closestPath, findNode } from \"./dom_traversal\";\n\nconst blockTagNames = [\n    \"ADDRESS\",\n    \"ARTICLE\",\n    \"ASIDE\",\n    \"BLOCKQUOTE\",\n    \"DETAILS\",\n    \"DIALOG\",\n    \"DD\",\n    \"DIV\",\n    \"DL\",\n    \"DT\",\n    \"FIELDSET\",\n    \"FIGCAPTION\",\n    \"FIGURE\",\n    \"FOOTER\",\n    \"FORM\",\n    \"H1\",\n    \"H2\",\n    \"H3\",\n    \"H4\",\n    \"H5\",\n    \"H6\",\n    \"HEADER\",\n    \"HGROUP\",\n    \"HR\",\n    \"LI\",\n    \"MAIN\",\n    \"NAV\",\n    \"OL\",\n    \"P\",\n    \"PRE\",\n    \"SECTION\",\n    \"TABLE\",\n    \"UL\",\n    // The following elements are not in the W3C list, for some reason.\n    \"SELECT\",\n    \"OPTION\",\n    \"TR\",\n    \"TD\",\n    \"TBODY\",\n    \"THEAD\",\n    \"TH\",\n];\n\nconst computedStyles = new WeakMap();\n\n/**\n * Return true if the given node is a block-level element, false otherwise.\n *\n * @param node\n */\nexport function isBlock(node) {\n    if (!node || node.nodeType !== Node.ELEMENT_NODE) {\n        return false;\n    }\n    const tagName = node.nodeName.toUpperCase();\n    if (tagName === \"BR\") {\n        // A <br> is always inline but getComputedStyle(br).display mistakenly\n        // returns 'block' if its parent is display:flex (at least on Chrome and\n        // Firefox (Linux)). Browsers normally support setting a <br>'s display\n        // property to 'none' but any other change is not supported. Therefore\n        // it is safe to simply declare that a <br> is never supposed to be a\n        // block.\n        return false;\n    }\n    // The node might not be in the DOM, in which case it has no CSS values.\n    if (!node.isConnected) {\n        return blockTagNames.includes(tagName);\n    }\n    // We won't call `getComputedStyle` more than once per node.\n    let style = computedStyles.get(node);\n    if (!style) {\n        style = node.ownerDocument.defaultView.getComputedStyle(node);\n        computedStyles.set(node, style);\n    }\n    if (style.display) {\n        return !style.display.includes(\"inline\") && style.display !== \"contents\";\n    }\n    return blockTagNames.includes(tagName);\n}\n\nexport function closestBlock(node) {\n    return findNode(closestPath(node), (node) => isBlock(node));\n}\n", "import { closestElement } from \"@html_editor/utils/dom_traversal\";\n\nexport const COLOR_PALETTE_COMPATIBILITY_COLOR_NAMES = [\n    \"primary\",\n    \"secondary\",\n    \"alpha\",\n    \"beta\",\n    \"gamma\",\n    \"delta\",\n    \"epsilon\",\n    \"success\",\n    \"info\",\n    \"warning\",\n    \"danger\",\n];\n\n/**\n * Colors of the default palette, used for substitution in shapes/illustrations.\n * key: number of the color in the palette (ie, o-color-<1-5>)\n * value: color hex code\n */\nexport const DEFAULT_PALETTE = {\n    1: \"#3AADAA\",\n    2: \"#7C6576\",\n    3: \"#F6F6F6\",\n    4: \"#FFFFFF\",\n    5: \"#383E45\",\n};\n\n/**\n * These constants are colors that can be edited by the user when using\n * web_editor in a website context. We keep track of them so that color\n * palettes and their preview elements can always have the right colors\n * displayed even if website has redefined the colors during an editing\n * session.\n *\n * @type {string[]}\n */\nexport const EDITOR_COLOR_CSS_VARIABLES = [...COLOR_PALETTE_COMPATIBILITY_COLOR_NAMES];\n\n// o-cc and o-colors\nfor (let i = 1; i <= 5; i++) {\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-color-${i}`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-bg`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-bg-gradient`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-headings`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-text`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-primary`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-primary-text`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-secondary`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-secondary-text`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-primary-border`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-secondary-border`);\n}\n\n// Grays\nfor (let i = 100; i <= 900; i += 100) {\n    EDITOR_COLOR_CSS_VARIABLES.push(`${i}`);\n}\n\n/**\n * Takes a color (rgb, rgba or hex) and returns its hex representation. If the\n * color is given in rgba, the background color of the node whose color we're\n * converting is used in conjunction with the alpha to compute the resulting\n * color (using the formula: `alpha*color + (1 - alpha)*background` for each\n * channel).\n *\n * @param {string} rgb\n * @param {HTMLElement} [node]\n * @returns {string} hexadecimal color (#RRGGBB)\n */\nexport function rgbToHex(rgb = \"\", node = null) {\n    if (rgb.startsWith(\"#\")) {\n        return rgb;\n    } else if (rgb.startsWith(\"rgba\")) {\n        const values = rgb.match(/[\\d.]{1,5}/g) || [];\n        const alpha = parseFloat(values.pop());\n        // Retrieve the background color.\n        let bgRgbValues = [];\n        if (node) {\n            let bgColor = getComputedStyle(node).backgroundColor;\n            if (bgColor.startsWith(\"rgba\")) {\n                // The background color is itself rgba so we need to compute\n                // the resulting color using the background color of its\n                // parent.\n                bgColor = rgbToHex(bgColor, node.parentElement);\n            }\n            if (bgColor && bgColor.startsWith(\"#\")) {\n                bgRgbValues = (bgColor.match(/[\\da-f]{2}/gi) || []).map((val) => parseInt(val, 16));\n            } else if (bgColor && bgColor.startsWith(\"rgb\")) {\n                bgRgbValues = (bgColor.match(/[\\d.]{1,5}/g) || []).map((val) => parseInt(val));\n            }\n        }\n        bgRgbValues = bgRgbValues.length ? bgRgbValues : [255, 255, 255]; // Default to white.\n\n        return (\n            \"#\" +\n            values\n                .map((value, index) => {\n                    const converted = Math.floor(\n                        alpha * parseInt(value) + (1 - alpha) * bgRgbValues[index]\n                    );\n                    const hex = parseInt(converted).toString(16);\n                    return hex.length === 1 ? \"0\" + hex : hex;\n                })\n                .join(\"\")\n        );\n    } else {\n        return (\n            \"#\" +\n            (rgb.match(/\\d{1,3}/g) || [])\n                .map((x) => {\n                    x = parseInt(x).toString(16);\n                    return x.length === 1 ? \"0\" + x : x;\n                })\n                .join(\"\")\n        );\n    }\n}\n\n/**\n * @param {string|number} name\n * @returns {boolean}\n */\nexport function isColorCombinationName(name) {\n    const number = parseInt(name);\n    return !isNaN(number) && number % 100 !== 0;\n}\n\n/**\n * @param {string} [value]\n * @returns {boolean}\n */\nexport function isColorGradient(value) {\n    return value && value.includes(\"-gradient(\");\n}\n\nexport const TEXT_CLASSES_REGEX = /\\btext-[^\\s]*\\b/;\nexport const BG_CLASSES_REGEX = /\\bbg-[^\\s]*\\b/;\n\n/**\n * Returns true if the given element has a visible color (fore- or\n * -background depending on the given mode).\n *\n * @param {Element} element\n * @param {string} mode 'color' or 'backgroundColor'\n * @returns {boolean}\n */\nexport function hasColor(element, mode) {\n    const style = element.style;\n    const parent = element.parentNode;\n    const classRegex = mode === \"color\" ? TEXT_CLASSES_REGEX : BG_CLASSES_REGEX;\n    if (isColorGradient(style[\"background-image\"])) {\n        if (element.classList.contains(\"text-gradient\")) {\n            if (mode === \"color\") {\n                return true;\n            }\n        } else {\n            if (mode !== \"color\") {\n                return true;\n            }\n        }\n    }\n    return (\n        (style[mode] &&\n            style[mode] !== \"inherit\" &&\n            (!parent || style[mode] !== parent.style[mode])) ||\n        (classRegex.test(element.className) &&\n            (!parent || getComputedStyle(element)[mode] !== getComputedStyle(parent)[mode]))\n    );\n}\n\n/**\n * Returns true if any given nodes has a visible color (fore- or\n * -background depending on the given mode).\n *\n * @param {array} nodes\n * @param {string} mode 'color' or 'backgroundColor'\n * @returns {boolean}\n */\nexport function hasAnyNodesColor(nodes, mode) {\n    for (const node of nodes) {\n        if (hasColor(closestElement(node), mode)) {\n            return true;\n        }\n    }\n    return false;\n}\n", "export const CTYPES = {\n    // Short for CONTENT_TYPES\n    // Inline group\n    CONTENT: 1,\n    SPACE: 2,\n\n    // Block group\n    BLOCK_OUTSIDE: 4,\n    BLOCK_INSIDE: 8,\n\n    // Br group\n    BR: 16,\n};\nexport function ctypeToString(ctype) {\n    return Object.keys(CTYPES).find((key) => CTYPES[key] === ctype);\n}\nexport const CTGROUPS = {\n    // Short for CONTENT_TYPE_GROUPS\n    INLINE: CTYPES.CONTENT | CTYPES.SPACE,\n    BLOCK: CTYPES.BLOCK_OUTSIDE | CTYPES.BLOCK_INSIDE,\n    BR: CTYPES.BR,\n};\n", "import { closestBlock, isBlock } from \"./blocks\";\nimport { isShrunkBlock, isVisible, paragraphRelatedElements } from \"./dom_info\";\nimport { callbacksForCursorUpdate } from \"./selection\";\nimport { isEmptyBlock, isPhrasingContent } from \"../utils/dom_info\";\nimport { childNodes } from \"./dom_traversal\";\n\n/** @typedef {import(\"@html_editor/core/selection_plugin\").Cursors} Cursors */\n\n/**\n * Take a node and unwrap all of its block contents recursively. All blocks\n * (except for firstChilds) are preceded by a <br> in order to preserve the line\n * breaks.\n *\n * @param {Node} node\n */\nexport function makeContentsInline(node) {\n    const document = node.ownerDocument;\n    let childIndex = 0;\n    for (const child of node.childNodes) {\n        if (isBlock(child)) {\n            if (childIndex && paragraphRelatedElements.includes(child.nodeName)) {\n                child.before(document.createElement(\"br\"));\n            }\n            for (const grandChild of child.childNodes) {\n                child.before(grandChild);\n                makeContentsInline(grandChild);\n            }\n            child.remove();\n        }\n        childIndex += 1;\n    }\n}\n\n/**\n * Wrap inline children nodes in Blocks, optionally updating cursors for\n * later selection restore. A paragraph is used for phrasing node, and a div\n * is used otherwise.\n *\n * @param {HTMLElement} element - block element\n * @param {Cursors} [cursors]\n */\nexport function wrapInlinesInBlocks(element, cursors = { update: () => {} }) {\n    // Helpers to manipulate preserving selection.\n    const wrapInBlock = (node, cursors) => {\n        const block = isPhrasingContent(node)\n            ? node.ownerDocument.createElement(\"P\")\n            : node.ownerDocument.createElement(\"DIV\");\n        cursors.update(callbacksForCursorUpdate.before(node, block));\n        node.before(block);\n        cursors.update(callbacksForCursorUpdate.append(block, node));\n        block.append(node);\n        return block;\n    };\n    const appendToCurrentBlock = (currentBlock, node, cursors) => {\n        if (currentBlock.tagName === \"P\" && !isPhrasingContent(node)) {\n            const block = document.createElement(\"DIV\");\n            cursors.update(callbacksForCursorUpdate.before(currentBlock, block));\n            currentBlock.before(block);\n            for (const child of [...currentBlock.childNodes]) {\n                cursors.update(callbacksForCursorUpdate.append(block, child));\n                block.append(child);\n            }\n            cursors.update(callbacksForCursorUpdate.remove(currentBlock));\n            currentBlock.remove();\n            currentBlock = block;\n        }\n        cursors.update(callbacksForCursorUpdate.append(currentBlock, node));\n        currentBlock.append(node);\n        return currentBlock;\n    };\n    const removeNode = (node, cursors) => {\n        cursors.update(callbacksForCursorUpdate.remove(node));\n        node.remove();\n    };\n\n    let currentBlock;\n    let shouldBreakLine = true;\n    for (const node of [...element.childNodes]) {\n        if (isBlock(node)) {\n            shouldBreakLine = true;\n        } else if (!isVisible(node)) {\n            removeNode(node, cursors);\n        } else if (node.nodeName === \"BR\") {\n            if (shouldBreakLine) {\n                wrapInBlock(node, cursors);\n            } else {\n                // BR preceded by inline content: discard it and make sure\n                // next inline goes in a new Block\n                removeNode(node, cursors);\n                shouldBreakLine = true;\n            }\n        } else if (shouldBreakLine) {\n            currentBlock = wrapInBlock(node, cursors);\n            shouldBreakLine = false;\n        } else {\n            currentBlock = appendToCurrentBlock(currentBlock, node, cursors);\n        }\n    }\n}\n\nexport function unwrapContents(node) {\n    const contents = childNodes(node);\n    for (const child of contents) {\n        node.parentNode.insertBefore(child, node);\n    }\n    node.parentNode.removeChild(node);\n    return contents;\n}\n\n// @todo @phoenix\n// This utils seem to handle a particular case of LI element.\n// If only relevant to the list plugin, a specific util should be created\n// that plugin instead.\nexport function setTagName(el, newTagName) {\n    const document = el.ownerDocument;\n    if (el.tagName === newTagName) {\n        return el;\n    }\n    const newEl = document.createElement(newTagName);\n    while (el.firstChild) {\n        newEl.append(el.firstChild);\n    }\n    if (el.tagName === \"LI\") {\n        el.append(newEl);\n    } else {\n        for (const attribute of el.attributes) {\n            newEl.setAttribute(attribute.name, attribute.value);\n        }\n        el.parentNode.replaceChild(newEl, el);\n    }\n    return newEl;\n}\n\n/**\n * Removes the specified class names from the given element.  If the element has\n * no more class names after removal, the \"class\" attribute is removed.\n *\n * @param {Element} element - The element from which to remove the class names.\n * @param {...string} classNames - The class names to be removed.\n */\nexport function removeClass(element, ...classNames) {\n    element.classList.remove(...classNames);\n    if (!element.classList.length) {\n        element.removeAttribute(\"class\");\n    }\n}\n\n/**\n * Add a BR in the given node if its closest ancestor block has nothing to make\n * it visible, and/or add a zero-width space in the given node if it's an empty\n * inline so the cursor can stay in it.\n *\n * @param {HTMLElement} el\n * @returns {Object} { br: the inserted <br> if any,\n *                     zws: the inserted zero-width space if any }\n */\nexport function fillEmpty(el) {\n    const document = el.ownerDocument;\n    const fillers = { ...fillShrunkPhrasingParent(el) };\n    if (!isBlock(el) && !isVisible(el) && !el.hasAttribute(\"data-oe-zws-empty-inline\")) {\n        const zws = document.createTextNode(\"\\u200B\");\n        el.appendChild(zws);\n        el.setAttribute(\"data-oe-zws-empty-inline\", \"\");\n        fillers.zws = zws;\n        const previousSibling = el.previousSibling;\n        if (previousSibling && previousSibling.nodeName === \"BR\") {\n            previousSibling.remove();\n        }\n    }\n    return fillers;\n}\n\n/**\n * Add a BR in a shrunk phrasing parent to make it visible.\n * A shrunk block is assumed to be a phrasing parent, and the inserted\n * <br> must be wrapped in a paragraph by the caller if necessary.\n *\n * @param {HTMLElement} el\n * @returns {Object} { br: the inserted <br> if any }\n */\nexport function fillShrunkPhrasingParent(el) {\n    const document = el.ownerDocument;\n    const fillers = {};\n    const blockEl = closestBlock(el);\n    if (isShrunkBlock(blockEl)) {\n        const br = document.createElement(\"br\");\n        blockEl.appendChild(br);\n        fillers.br = br;\n    }\n    return fillers;\n}\n\n/**\n * Removes a trailing BR if it is unnecessary:\n * in a non-empty block, if the last childNode is a BR and its previous sibling\n * is not a BR, remove the BR.\n *\n * @param {HTMLElement} el\n * @returns {HTMLElement|undefined} the removed br, if any\n */\nexport function cleanTrailingBR(el) {\n    const candidate = el?.lastChild;\n    if (\n        candidate?.nodeName === \"BR\" &&\n        candidate.previousSibling?.nodeName !== \"BR\" &&\n        !isEmptyBlock(el)\n    ) {\n        candidate.remove();\n        return candidate;\n    }\n}\n\nexport function toggleClass(node, className) {\n    node.classList.toggle(className);\n    if (!node.className) {\n        node.removeAttribute(\"class\");\n    }\n}\n\n/**\n * Remove all occurrences of a character from a text node and optionally update\n * cursors for later selection restore.\n *\n * @param {Node} node text node\n * @param {String} char character to remove (string of length 1)\n * @param {Cursors} [cursors]\n */\nexport function cleanTextNode(node, char, cursors) {\n    const removedIndexes = [];\n    node.textContent = node.textContent.replaceAll(char, (_, offset) => {\n        removedIndexes.push(offset);\n        return \"\";\n    });\n    cursors?.update((cursor) => {\n        if (cursor.node === node) {\n            cursor.offset -= removedIndexes.filter((index) => cursor.offset > index).length;\n        }\n    });\n}\n", "import { closestBlock, isBlock } from \"./blocks\";\nimport { closestElement, firstLeaf, lastLeaf } from \"./dom_traversal\";\nimport { DIRECTIONS, nodeSize } from \"./position\";\n\nexport function isEmpty(el) {\n    if (isProtecting(el) || isProtected(el)) {\n        return false;\n    }\n    const content = el.innerHTML.trim();\n    if (content === \"\" || content === \"<br>\") {\n        return true;\n    }\n    return false;\n}\n\n/**\n * Return true if the given node appears bold. The node is considered to appear\n * bold if its font weight is bigger than 500 (eg.: Heading 1), or if its font\n * weight is bigger than that of its closest block.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isBold(node) {\n    const fontWeight = +getComputedStyle(closestElement(node)).fontWeight;\n    return fontWeight > 500 || fontWeight > +getComputedStyle(closestBlock(node)).fontWeight;\n}\n\n/**\n * Return true if the given node appears italic.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isItalic(node) {\n    return getComputedStyle(closestElement(node)).fontStyle === \"italic\";\n}\n\n/**\n * Return true if the given node appears underlined.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isUnderline(node) {\n    let parent = closestElement(node);\n    while (parent) {\n        if (getComputedStyle(parent).textDecorationLine.includes(\"underline\")) {\n            return true;\n        }\n        parent = parent.parentElement;\n    }\n    return false;\n}\n\n/**\n * Return true if the given node appears struck through.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isStrikeThrough(node) {\n    let parent = closestElement(node);\n    while (parent) {\n        if (getComputedStyle(parent).textDecorationLine.includes(\"line-through\")) {\n            return true;\n        }\n        parent = parent.parentElement;\n    }\n    return false;\n}\n\n/**\n * Return true if the given node font-size is equal to `props.size`.\n *\n * @param {Object} props\n * @param {Node} props.node A node to compare the font-size against.\n * @param {String} props.size The font-size value of the node that will be\n *     checked against.\n * @returns {boolean}\n */\nexport function isFontSize(node, props) {\n    const element = closestElement(node);\n    return getComputedStyle(element)[\"font-size\"] === props.size;\n}\n\n/**\n * Return true if the given node classlist contains `props.className`.\n *\n * @param {Object} props\n * @param {Node} node A node to compare the font-size against.\n * @param {String} props.className The name of the class.\n * @returns {boolean}\n */\nexport function hasClass(node, props) {\n    const element = closestElement(node);\n    return element.classList.contains(props.className);\n}\n\n/**\n * Return true if the given node appears in a different direction than that of\n * the editable ('ltr' or 'rtl').\n *\n * Note: The direction of the editable is set on its \"dir\" attribute, to the\n * value of the \"direction\" option on instantiation of the editor.\n *\n * @param {Node} node\n * @param {Element} editable\n * @returns {boolean}\n */\nexport function isDirectionSwitched(node, editable) {\n    const defaultDirection = editable.getAttribute(\"dir\") || \"ltr\";\n    return getComputedStyle(closestElement(node)).direction !== defaultDirection;\n}\n\n// /**\n//  * Return true if the given node is a row element.\n//  */\nexport function isRow(node) {\n    return [\"TH\", \"TD\"].includes(node.tagName);\n}\n\nexport function isZWS(node) {\n    return node && node.textContent === \"\\u200B\";\n}\n\n/**\n * Returns true if the given node is in a PRE context for whitespace handling.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isInPre(node) {\n    const element = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;\n    return (\n        !!element &&\n        (!!element.closest(\"pre\") ||\n            getComputedStyle(element).getPropertyValue(\"white-space\") === \"pre\")\n    );\n}\n\nexport const ZERO_WIDTH_CHARS = [\"\\u200b\", \"\\ufeff\"];\n\nexport const whitespace = `[^\\\\S\\\\u00A0\\\\u0009\\\\ufeff]`; // for formatting (no \"real\" content) (TODO: 0009 shouldn't be included)\nconst whitespaceRegex = new RegExp(`^${whitespace}*$`);\nexport function isWhitespace(value) {\n    const str = typeof value === \"string\" ? value : value.nodeValue;\n    return whitespaceRegex.test(str);\n}\n\n// eslint-disable-next-line no-control-regex\nconst visibleCharRegex = /[^\\s\\u200b]|[\\u00A0\\u0009]$/; // contains at least a char that is always visible (TODO: 0009 shouldn't be included)\nexport function isVisibleTextNode(testedNode) {\n    if (!testedNode || !testedNode.length || testedNode.nodeType !== Node.TEXT_NODE) {\n        return false;\n    }\n    if (isProtected(testedNode)) {\n        return true;\n    }\n    if (\n        visibleCharRegex.test(testedNode.textContent) ||\n        (isInPre(testedNode) && isWhitespace(testedNode))\n    ) {\n        return true;\n    }\n    if (ZERO_WIDTH_CHARS.includes(testedNode.textContent)) {\n        return false; // a ZW(NB)SP is always invisible, regardless of context.\n    }\n    // The following assumes node is made entirely of whitespace and is not\n    // preceded of followed by a block.\n    // Find out contiguous preceding and following text nodes\n    let preceding;\n    let following;\n    // Control variable to know whether the current node has been found\n    let foundTestedNode;\n    const currentNodeParentBlock = closestBlock(testedNode);\n    if (!currentNodeParentBlock) {\n        return false;\n    }\n    const nodeIterator = document.createNodeIterator(currentNodeParentBlock);\n    for (let node = nodeIterator.nextNode(); node; node = nodeIterator.nextNode()) {\n        if (node.nodeType === Node.TEXT_NODE) {\n            // If we already found the tested node, the current node is the\n            // contiguous following, and we can stop looping\n            // If the current node is the tested node, mark it as found and\n            // continue.\n            // If we haven't reached the tested node, overwrite the preceding\n            // node.\n            if (foundTestedNode) {\n                following = node;\n                break;\n            } else if (testedNode === node) {\n                foundTestedNode = true;\n            } else {\n                preceding = node;\n            }\n        } else if (isBlock(node)) {\n            // If we found the tested node, then the following node is irrelevant\n            // If we didn't, then the current preceding node is irrelevant\n            if (foundTestedNode) {\n                break;\n            } else {\n                preceding = null;\n            }\n        } else if (foundTestedNode && !isWhitespace(node)) {\n            // <block>space<inline>text</inline></block> -> space is visible\n            following = node;\n            break;\n        }\n    }\n    while (following && !visibleCharRegex.test(following.textContent)) {\n        following = following.nextSibling;\n    }\n    // Missing preceding or following: invisible.\n    // Preceding or following not in the same block as tested node: invisible.\n    if (\n        !(preceding && following) ||\n        currentNodeParentBlock !== closestBlock(preceding) ||\n        currentNodeParentBlock !== closestBlock(following)\n    ) {\n        return false;\n    }\n    // Preceding is whitespace or following is whitespace: invisible\n    return visibleCharRegex.test(preceding.textContent);\n}\n\n/**\n * Returns whether the given node is a element that could be considered to be\n * removed by itself = self closing tags.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nconst selfClosingElementTags = [\"BR\", \"IMG\", \"INPUT\", \"T\", \"HR\"];\nexport function isSelfClosingElement(node) {\n    return node && selfClosingElementTags.includes(node.nodeName);\n}\n\n/**\n * Returns whether removing the given node from the DOM will have a visible\n * effect or not.\n *\n * Note: TODO this is not handling all cases right now, just the ones the\n * caller needs at the moment. For example a space text node between two inlines\n * will always return 'true' while it is sometimes invisible.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isVisible(node) {\n    return (\n        !!node &&\n        ((node.nodeType === Node.TEXT_NODE && isVisibleTextNode(node)) ||\n            isSelfClosingElement(node) ||\n            // @todo: handle it in resources?\n            isMediaElement(node) ||\n            hasVisibleContent(node) ||\n            isProtecting(node))\n    );\n}\nexport function hasVisibleContent(node) {\n    return [...(node?.childNodes || [])].some((n) => isVisible(n));\n}\n\nexport function isZwnbsp(node) {\n    return node?.nodeType === Node.TEXT_NODE && node.textContent === \"\\ufeff\";\n}\n\nexport function isTangible(node) {\n    return isVisible(node) || isZwnbsp(node) || hasTangibleContent(node);\n}\n\nexport function hasTangibleContent(node) {\n    return [...(node?.childNodes || [])].some((n) => isTangible(n));\n}\n\nexport const isNotEditableNode = (node) =>\n    node.getAttribute &&\n    node.getAttribute(\"contenteditable\") &&\n    node.getAttribute(\"contenteditable\").toLowerCase() === \"false\";\n\nconst iconTags = [\"I\", \"SPAN\"];\n// @todo @phoenix: move the specific part in a proper plugin.\nconst iconClasses = [\"fa\", \"fab\", \"fad\", \"far\", \"oi\"];\n\nexport const ICON_SELECTOR = iconTags\n    .map((tag) => {\n        return iconClasses\n            .map((cls) => {\n                return `${tag}.${cls}`;\n            })\n            .join(\", \");\n    })\n    .join(\", \");\n\n/**\n * Indicates if the given node is an icon element.\n *\n * @see ICON_SELECTOR\n * @param {?Node} [node]\n * @returns {boolean}\n */\nexport function isIconElement(node) {\n    return !!(\n        node &&\n        iconTags.includes(node.nodeName) &&\n        iconClasses.some((cls) => node.classList.contains(cls))\n    );\n}\n// @todo @phoenix: move the specific part in a proper plugin.\nexport function isMediaElement(node) {\n    return (\n        isIconElement(node) ||\n        (node.classList &&\n            (node.classList.contains(\"o_image\") || node.classList.contains(\"media_iframe_video\")))\n    );\n}\n\n// See https://developer.mozilla.org/en-US/docs/Web/HTML/Content_categories#phrasing_content\nconst phrasingTagNames = new Set([\n    \"ABBR\",\n    \"AUDIO\",\n    \"B\",\n    \"BDI\",\n    \"BDO\",\n    \"BR\",\n    \"BUTTON\",\n    \"CANVAS\",\n    \"CITE\",\n    \"CODE\",\n    \"DATA\",\n    \"DATALIST\",\n    \"DFN\",\n    \"EM\",\n    \"EMBED\",\n    \"I\",\n    \"IFRAME\",\n    \"IMG\",\n    \"INPUT\",\n    \"KBD\",\n    \"LABEL\",\n    \"MARK\",\n    \"MATH\",\n    \"METER\",\n    \"NOSCRIPT\",\n    \"OBJECT\",\n    \"OUTPUT\",\n    \"PICTURE\",\n    \"PROGRESS\",\n    \"Q\",\n    \"RUBY\",\n    \"S\",\n    \"SAMP\",\n    \"SCRIPT\",\n    \"SELECT\",\n    \"SLOT\",\n    \"SMALL\",\n    \"SPAN\",\n    \"STRONG\",\n    \"SUB\",\n    \"SUP\",\n    \"SVG\",\n    \"TEMPLATE\",\n    \"TEXTAREA\",\n    \"TIME\",\n    \"U\",\n    \"VAR\",\n    \"VIDEO\",\n    \"WBR\",\n    \"FONT\", // TODO @phoenix: font is deprecated, replace usage\n    // The following elements are phrasing content under specific conditions,\n    // evaluate if those conditions are applicable when using this set.\n    \"A\",\n    \"AREA\",\n    \"DEL\",\n    \"INS\",\n    \"LINK\",\n    \"MAP\",\n    \"META\",\n]);\n\nexport function isPhrasingContent(node) {\n    if (\n        node &&\n        (node.nodeType === Node.TEXT_NODE ||\n            (node.nodeType === Node.ELEMENT_NODE && phrasingTagNames.has(node.tagName)))\n    ) {\n        return true;\n    }\n    return false;\n}\n\n/**\n * A \"protected\" node will have its mutations filtered and not be registered\n * in an history step. Some editor features like selection handling, command\n * hint, toolbar, tooltip, etc. are also disabled. Protected roots have their\n * data-oe-protected attribute set to either \"\" or \"true\". If the closest parent\n * with a data-oe-protected attribute has the value \"false\", it is not\n * protected. Unknown values are ignored.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isProtected(node) {\n    if (!node) {\n        return false;\n    }\n    const candidate = node.parentElement\n        ? closestElement(node.parentElement, \"[data-oe-protected]\")\n        : null;\n    if (!candidate || candidate.dataset.oeProtected === \"false\") {\n        return false;\n    }\n    return true;\n}\n\n/**\n * A \"protecting\" element contains childNodes that are protected.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isProtecting(node) {\n    if (!node) {\n        return false;\n    }\n    return (\n        node.nodeType === Node.ELEMENT_NODE &&\n        node.dataset.oeProtected !== \"false\" &&\n        node.dataset.oeProtected !== undefined\n    );\n}\n\nexport function isUnprotecting(node) {\n    if (!node) {\n        return false;\n    }\n    return node.nodeType === Node.ELEMENT_NODE && node.dataset.oeProtected === \"false\";\n}\n\n// This is a list of \"paragraph-related elements\", defined as elements that\n// behave like paragraphs.\nexport const paragraphRelatedElements = [\n    \"P\",\n    \"H1\",\n    \"H2\",\n    \"H3\",\n    \"H4\",\n    \"H5\",\n    \"H6\",\n    \"PRE\",\n    \"BLOCKQUOTE\",\n];\n\n/**\n * Return true if the given node allows \"paragraph-related elements\".\n *\n * @see paragraphRelatedElements\n * @param {Node} node\n * @returns {boolean}\n */\nexport function allowsParagraphRelatedElements(node) {\n    return isBlock(node) && !paragraphRelatedElements.includes(node.nodeName);\n}\n\nexport const phrasingContent = new Set([\"#text\", ...phrasingTagNames]);\nconst flowContent = new Set([...phrasingContent, ...paragraphRelatedElements, \"DIV\", \"HR\"]);\nconst listItem = new Set([\"LI\"]);\n\nconst allowedContent = {\n    BLOCKQUOTE: phrasingContent, // HTML spec: flow content\n    DIV: flowContent,\n    H1: phrasingContent,\n    H2: phrasingContent,\n    H3: phrasingContent,\n    H4: phrasingContent,\n    H5: phrasingContent,\n    H6: phrasingContent,\n    HR: new Set(),\n    LI: flowContent,\n    OL: listItem,\n    UL: listItem,\n    P: phrasingContent,\n    PRE: phrasingContent,\n    TD: flowContent,\n    TR: new Set([\"TD\"]),\n};\n\n/**\n * @param {Element} parentBlock\n * @param {Node[]} nodes\n * @returns {boolean}\n */\nexport function isAllowedContent(parentBlock, nodes) {\n    const allowedContentSet = allowedContent[parentBlock.nodeName];\n    if (!allowedContentSet) {\n        // Spec: a block not listed in allowedContent allows anything.\n        // See \"custom-block\" in tests.\n        return true;\n    }\n    return nodes.every((node) => allowedContentSet.has(node.nodeName));\n}\n\n/**\n * Checks whether or not the given block has any visible content, except for\n * a placeholder BR.\n *\n * @param {HTMLElement} blockEl\n * @returns {boolean}\n */\nexport function isEmptyBlock(blockEl) {\n    if (!blockEl || blockEl.nodeType !== Node.ELEMENT_NODE) {\n        return false;\n    }\n    if (visibleCharRegex.test(blockEl.textContent)) {\n        return false;\n    }\n    if (blockEl.querySelectorAll(\"br\").length >= 2) {\n        return false;\n    }\n    if (isProtecting(blockEl) || isProtected(blockEl)) {\n        // Protecting nodes should never be considered empty for editor\n        // operations, as their content is a \"black box\". Their content should\n        // be managed by a specialized plugin.\n        return false;\n    }\n    const nodes = blockEl.querySelectorAll(\"*\");\n    for (const node of nodes) {\n        // There is no text and no double BR, the only thing that could make\n        // this visible is a \"visible empty\" node like an image.\n        if (\n            node.nodeName != \"BR\" &&\n            (isSelfClosingElement(node) || isMediaElement(node) || isProtecting(node))\n        ) {\n            return false;\n        }\n    }\n    return isBlock(blockEl);\n}\n/**\n * Checks whether or not the given block element has something to make it have\n * a visible height (except for padding / border).\n *\n * @param {HTMLElement} blockEl\n * @returns {boolean}\n */\nexport function isShrunkBlock(blockEl) {\n    return isEmptyBlock(blockEl) && !blockEl.querySelector(\"br\") && !isSelfClosingElement(blockEl);\n}\n\nexport function isEditorTab(node) {\n    return node && node.nodeName === \"SPAN\" && node.classList.contains(\"oe-tabs\");\n}\n\nexport function getDeepestPosition(node, offset) {\n    let direction = DIRECTIONS.RIGHT;\n    let next = node;\n    while (next) {\n        if (isTangible(next) || (isZWS(next) && isContentEditable(next))) {\n            // Valid node: update position then try to go deeper.\n            if (next !== node) {\n                [node, offset] = [next, direction ? 0 : nodeSize(next)];\n            }\n            // First switch direction to left if offset is at the end.\n            direction = offset < node.childNodes.length;\n            next = node.childNodes[direction ? offset : offset - 1];\n        } else if (direction && next.nextSibling && closestBlock(node).contains(next.nextSibling)) {\n            // Invalid node: skip to next sibling (without crossing blocks).\n            next = next.nextSibling;\n        } else {\n            // Invalid node: skip to previous sibling (without crossing blocks).\n            direction = DIRECTIONS.LEFT;\n            next = closestBlock(node).contains(next.previousSibling) && next.previousSibling;\n        }\n        // Avoid too-deep ranges inside self-closing elements like [BR, 0].\n        next = !isSelfClosingElement(next) && next;\n    }\n    return [node, offset];\n}\n\nexport function previousLeaf(node, editable, skipInvisible = false) {\n    let ancestor = node;\n    while (ancestor && !ancestor.previousSibling && ancestor !== editable) {\n        ancestor = ancestor.parentElement;\n    }\n    if (ancestor && ancestor !== editable) {\n        if (skipInvisible && !isVisible(ancestor.previousSibling)) {\n            return previousLeaf(ancestor.previousSibling, editable, skipInvisible);\n        } else {\n            const last = lastLeaf(ancestor.previousSibling);\n            if (skipInvisible && !isVisible(last)) {\n                return previousLeaf(last, editable, skipInvisible);\n            } else {\n                return last;\n            }\n        }\n    }\n}\nexport function nextLeaf(node, editable, skipInvisible = false) {\n    let ancestor = node;\n    while (ancestor && !ancestor.nextSibling && ancestor !== editable) {\n        ancestor = ancestor.parentElement;\n    }\n    if (ancestor && ancestor !== editable) {\n        if (skipInvisible && ancestor.nextSibling && !isVisible(ancestor.nextSibling)) {\n            return nextLeaf(ancestor.nextSibling, editable, skipInvisible);\n        } else {\n            const first = firstLeaf(ancestor.nextSibling);\n            if (skipInvisible && !isVisible(first)) {\n                return nextLeaf(first, editable, skipInvisible);\n            } else {\n                return first;\n            }\n        }\n    }\n}\n\nfunction hasPseudoElementContent(node, pseudoSelector) {\n    const content = getComputedStyle(node, pseudoSelector).getPropertyValue(\"content\");\n    return content && content !== \"none\";\n}\n\nconst NOT_A_NUMBER = /[^\\d]/g;\n\nexport function areSimilarElements(node, node2) {\n    if (![node, node2].every((n) => n?.nodeType === Node.ELEMENT_NODE)) {\n        return false; // The nodes don't both exist or aren't both elements.\n    }\n    if (node.nodeName !== node2.nodeName) {\n        return false; // The nodes aren't the same type of element.\n    }\n    for (const name of new Set([...node.getAttributeNames(), ...node2.getAttributeNames()])) {\n        if (node.getAttribute(name) !== node2.getAttribute(name)) {\n            return false; // The nodes don't have the same attributes.\n        }\n    }\n    if (\n        [node, node2].some(\n            (n) => hasPseudoElementContent(n, \":before\") || hasPseudoElementContent(n, \":after\")\n        )\n    ) {\n        return false; // The nodes have pseudo elements with content.\n    }\n    if (isBlock(node)) {\n        return false;\n    }\n    const nodeStyle = getComputedStyle(node);\n    const node2Style = getComputedStyle(node2);\n    return (\n        !+nodeStyle.padding.replace(NOT_A_NUMBER, \"\") &&\n        !+node2Style.padding.replace(NOT_A_NUMBER, \"\") &&\n        !+nodeStyle.margin.replace(NOT_A_NUMBER, \"\") &&\n        !+node2Style.margin.replace(NOT_A_NUMBER, \"\")\n    );\n}\n\nexport function isTextNode(node) {\n    return node.nodeType === Node.TEXT_NODE;\n}\n\nexport function isContentEditable(node) {\n    const element = isTextNode(node) ? node.parentElement : node;\n    return element.isContentEditable;\n}\n", "import { isBlock } from \"./blocks\";\nimport { CTGROUPS, CTYPES, ctypeToString } from \"./content_types\";\nimport { isInPre, isVisible, isWhitespace, whitespace } from \"./dom_info\";\nimport {\n    PATH_END_REASONS,\n    ancestors,\n    closestElement,\n    closestPath,\n    createDOMPathGenerator,\n} from \"./dom_traversal\";\nimport { DIRECTIONS, leftPos, rightPos } from \"./position\";\n\nconst prepareUpdateLockedEditables = new Set();\n/**\n * Any editor command is applied to a selection (collapsed or not). After the\n * command, the content type on the selection boundaries, in both direction,\n * should be preserved (some whitespace should disappear as went from collapsed\n * to non collapsed, or converted to &nbsp; as went from non collapsed to\n * collapsed, there also <br> to remove/duplicate, etc).\n *\n * This function returns a callback which allows to do that after the command\n * has been done.\n *\n * Note: the method has been made generic enough to work with non-collapsed\n * selection but can be used for an unique cursor position.\n *\n * @param {HTMLElement} el\n * @param {number} offset\n * @param {...(HTMLElement|number)} args - argument 1 and 2 can be repeated for\n *     multiple preparations with only one restore callback returned. Note: in\n *     that case, the positions should be given in the document node order.\n * @param {Object} [options]\n * @param {boolean} [options.allowReenter = true] - if false, all calls to\n *     prepareUpdate before this one gets restored will be ignored.\n * @param {string} [options.label = <random 6 character string>]\n * @param {boolean} [options.debug = false] - if true, adds nicely formatted\n *     console logs to help with debugging.\n * @returns {function}\n */\nexport function prepareUpdate(...args) {\n    const closestRoot =\n        args.length &&\n        ancestors(args[0]).find((ancestor) => ancestor.classList.contains(\"odoo-editor-editable\"));\n    const isPrepareUpdateLocked = closestRoot && prepareUpdateLockedEditables.has(closestRoot);\n    const hash = (Math.random() + 1).toString(36).substring(7);\n    const options = {\n        allowReenter: true,\n        label: hash,\n        debug: false,\n        ...(args.length && args[args.length - 1] instanceof Object ? args.pop() : {}),\n    };\n    if (options.debug) {\n        console.log(\n            \"%cPreparing%c update: \" +\n                options.label +\n                (options.label === hash ? \"\" : ` (${hash})`) +\n                \"%c\" +\n                (isPrepareUpdateLocked ? \" LOCKED\" : \"\"),\n            \"color: cyan;\",\n            \"color: white;\",\n            \"color: red; font-weight: bold;\"\n        );\n    }\n    if (isPrepareUpdateLocked) {\n        return () => {\n            if (options.debug) {\n                console.log(\n                    \"%cRestoring%c update: \" +\n                        options.label +\n                        (options.label === hash ? \"\" : ` (${hash})`) +\n                        \"%c LOCKED\",\n                    \"color: lightgreen;\",\n                    \"color: white;\",\n                    \"color: red; font-weight: bold;\"\n                );\n            }\n        };\n    }\n    if (!options.allowReenter && closestRoot) {\n        prepareUpdateLockedEditables.add(closestRoot);\n    }\n    const positions = [...args];\n\n    // Check the state in each direction starting from each position.\n    const restoreData = [];\n    let el, offset;\n    while (positions.length) {\n        // Note: important to get the positions in reverse order to restore\n        // right side before left side.\n        offset = positions.pop();\n        el = positions.pop();\n        const left = getState(el, offset, DIRECTIONS.LEFT);\n        const right = getState(el, offset, DIRECTIONS.RIGHT, left.cType);\n        if (options.debug) {\n            const editable = el && closestElement(el, \".odoo-editor-editable\");\n            const oldEditableHTML =\n                (editable && editable.innerHTML.replaceAll(\" \", \"_\").replaceAll(\"\\u200B\", \"ZWS\")) ||\n                \"\";\n            left.oldEditableHTML = oldEditableHTML;\n            right.oldEditableHTML = oldEditableHTML;\n        }\n        restoreData.push(left, right);\n    }\n\n    // Create the callback that will be able to restore the state in each\n    // direction wherever the node in the opposite direction has landed.\n    return function restoreStates() {\n        if (options.debug) {\n            console.log(\n                \"%cRestoring%c update: \" +\n                    options.label +\n                    (options.label === hash ? \"\" : ` (${hash})`),\n                \"color: lightgreen;\",\n                \"color: white;\"\n            );\n        }\n        for (const data of restoreData) {\n            restoreState(data, options.debug);\n        }\n        if (!options.allowReenter && closestRoot) {\n            prepareUpdateLockedEditables.delete(closestRoot);\n        }\n    };\n}\n\nexport const leftLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.LEFT, {\n    leafOnly: true,\n    stopTraverseFunction: isBlock,\n    stopFunction: isBlock,\n});\n\nconst rightLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.RIGHT, {\n    leafOnly: true,\n    stopTraverseFunction: isBlock,\n    stopFunction: isBlock,\n});\n\n/**\n * Retrieves the \"state\" from a given position looking at the given direction.\n * The \"state\" is the type of content. The functions also returns the first\n * meaninful node looking in the opposite direction = the first node we trust\n * will not disappear if a command is played in the given direction.\n *\n * Note: only work for in-between nodes positions. If the position is inside a\n * text node, first split it @see splitTextNode.\n *\n * @param {HTMLElement} el\n * @param {number} offset\n * @param {boolean} direction @see DIRECTIONS.LEFT @see DIRECTIONS.RIGHT\n * @param {CTYPES} [leftCType]\n * @returns {Object}\n */\nexport function getState(el, offset, direction, leftCType) {\n    const leftDOMPath = leftLeafOnlyNotBlockPath;\n    const rightDOMPath = rightLeafOnlyNotBlockPath;\n\n    let domPath;\n    let inverseDOMPath;\n    const whitespaceAtStartRegex = new RegExp(\"^\" + whitespace + \"+\");\n    const whitespaceAtEndRegex = new RegExp(whitespace + \"+$\");\n    const reasons = [];\n    if (direction === DIRECTIONS.LEFT) {\n        domPath = leftDOMPath(el, offset, reasons);\n        inverseDOMPath = rightDOMPath(el, offset);\n    } else {\n        domPath = rightDOMPath(el, offset, reasons);\n        inverseDOMPath = leftDOMPath(el, offset);\n    }\n\n    // TODO I think sometimes, the node we have to consider as the\n    // anchor point to restore the state is not the first one of the inverse\n    // path (like for example, empty text nodes that may disappear\n    // after the command so we would not want to get those ones).\n    const boundaryNode = inverseDOMPath.next().value;\n\n    // We only traverse through deep inline nodes. If we cannot find a\n    // meanfingful state between them, that means we hit a block.\n    let cType = undefined;\n\n    // Traverse the DOM in the given direction to check what type of content\n    // there is.\n    let lastSpace = null;\n    for (const node of domPath) {\n        if (node.nodeType === Node.TEXT_NODE) {\n            const value = node.nodeValue;\n            // If we hit a text node, the state depends on the path direction:\n            // any space encountered backwards is a visible space if we hit\n            // visible content afterwards. If going forward, spaces are only\n            // visible if we have content backwards.\n            if (direction === DIRECTIONS.LEFT) {\n                if (!isWhitespace(value)) {\n                    if (lastSpace) {\n                        cType = CTYPES.SPACE;\n                    } else {\n                        const rightLeaf = rightLeafOnlyNotBlockPath(node).next().value;\n                        const hasContentRight =\n                            rightLeaf && !whitespaceAtStartRegex.test(rightLeaf.textContent);\n                        cType =\n                            !hasContentRight && whitespaceAtEndRegex.test(node.textContent)\n                                ? CTYPES.SPACE\n                                : CTYPES.CONTENT;\n                    }\n                    break;\n                }\n                if (value.length) {\n                    lastSpace = node;\n                }\n            } else {\n                leftCType = leftCType || getState(el, offset, DIRECTIONS.LEFT).cType;\n                if (whitespaceAtStartRegex.test(value)) {\n                    const leftLeaf = leftLeafOnlyNotBlockPath(node).next().value;\n                    const hasContentLeft =\n                        leftLeaf && !whitespaceAtEndRegex.test(leftLeaf.textContent);\n                    const rct = !isWhitespace(value)\n                        ? CTYPES.CONTENT\n                        : getState(...rightPos(node), DIRECTIONS.RIGHT).cType;\n                    cType =\n                        leftCType & CTYPES.CONTENT &&\n                        rct & (CTYPES.CONTENT | CTYPES.BR) &&\n                        !hasContentLeft\n                            ? CTYPES.SPACE\n                            : rct;\n                    break;\n                }\n                if (!isWhitespace(value)) {\n                    cType = CTYPES.CONTENT;\n                    break;\n                }\n            }\n        } else if (node.nodeName === \"BR\") {\n            cType = CTYPES.BR;\n            break;\n        } else if (isVisible(node)) {\n            // E.g. an image\n            cType = CTYPES.CONTENT;\n            break;\n        }\n    }\n\n    if (cType === undefined) {\n        cType = reasons.includes(PATH_END_REASONS.BLOCK_HIT)\n            ? CTYPES.BLOCK_OUTSIDE\n            : CTYPES.BLOCK_INSIDE;\n    }\n\n    return {\n        node: boundaryNode,\n        direction: direction,\n        cType: cType, // Short for contentType\n    };\n}\nconst priorityRestoreStateRules = [\n    // Each entry is a list of two objects, with each key being optional (the\n    // more key-value pairs, the bigger the priority).\n    // {direction: ..., cType1: ..., cType2: ...}\n    // ->\n    // {spaceVisibility: (false|true), brVisibility: (false|true)}\n    [\n        // Replace a space by &nbsp; when it was not collapsed before and now is\n        // collapsed (one-letter word removal for example).\n        { cType1: CTYPES.CONTENT, cType2: CTYPES.SPACE | CTGROUPS.BLOCK },\n        { spaceVisibility: true },\n    ],\n    [\n        // Replace a space by &nbsp; when it was content before and now it is\n        // a BR.\n        { direction: DIRECTIONS.LEFT, cType1: CTGROUPS.INLINE, cType2: CTGROUPS.BR },\n        { spaceVisibility: true },\n    ],\n    [\n        // Replace a space by &nbsp; when it was content before and now it is\n        // a BR (removal of last character before a BR for example).\n        { direction: DIRECTIONS.RIGHT, cType1: CTGROUPS.CONTENT, cType2: CTGROUPS.BR },\n        { spaceVisibility: true },\n    ],\n    [\n        // Replace a space by &nbsp; when it was visible thanks to a BR which\n        // is now gone.\n        { direction: DIRECTIONS.RIGHT, cType1: CTGROUPS.BR, cType2: CTYPES.SPACE | CTGROUPS.BLOCK },\n        { spaceVisibility: true },\n    ],\n    [\n        // Remove all collapsed spaces when a space is removed.\n        { cType1: CTYPES.SPACE },\n        { spaceVisibility: false },\n    ],\n    [\n        // Remove spaces once the preceeding BR is removed\n        { direction: DIRECTIONS.LEFT, cType1: CTGROUPS.BR },\n        { spaceVisibility: false },\n    ],\n    [\n        // Remove space before block once content is put after it (otherwise it\n        // would become visible).\n        { cType1: CTGROUPS.BLOCK, cType2: CTGROUPS.INLINE | CTGROUPS.BR },\n        { spaceVisibility: false },\n    ],\n    [\n        // Duplicate a BR once the content afterwards disappears\n        { direction: DIRECTIONS.RIGHT, cType1: CTGROUPS.INLINE, cType2: CTGROUPS.BLOCK },\n        { brVisibility: true },\n    ],\n    [\n        // Remove a BR at the end of a block once inline content is put after\n        // it (otherwise it would act as a line break).\n        {\n            direction: DIRECTIONS.RIGHT,\n            cType1: CTGROUPS.BLOCK,\n            cType2: CTGROUPS.INLINE | CTGROUPS.BR,\n        },\n        { brVisibility: false },\n    ],\n    [\n        // Remove a BR once the BR that preceeds it is now replaced by\n        // content (or if it was a BR at the start of a block which now is\n        // a trailing BR).\n        {\n            direction: DIRECTIONS.LEFT,\n            cType1: CTGROUPS.BR | CTGROUPS.BLOCK,\n            cType2: CTGROUPS.INLINE,\n        },\n        { brVisibility: false, extraBRRemovalCondition: (brNode) => isFakeLineBreak(brNode) },\n    ],\n];\nfunction restoreStateRuleHashCode(direction, cType1, cType2) {\n    return `${direction}-${cType1}-${cType2}`;\n}\nconst allRestoreStateRules = (function () {\n    const map = new Map();\n\n    const keys = [\"direction\", \"cType1\", \"cType2\"];\n    for (const direction of Object.values(DIRECTIONS)) {\n        for (const cType1 of Object.values(CTYPES)) {\n            for (const cType2 of Object.values(CTYPES)) {\n                const rule = { direction: direction, cType1: cType1, cType2: cType2 };\n\n                // Search for the rules which match whatever their priority\n                const matchedRules = [];\n                for (const entry of priorityRestoreStateRules) {\n                    let priority = 0;\n                    for (const key of keys) {\n                        const entryKeyValue = entry[0][key];\n                        if (entryKeyValue !== undefined) {\n                            if (\n                                typeof entryKeyValue === \"boolean\"\n                                    ? rule[key] === entryKeyValue\n                                    : rule[key] & entryKeyValue\n                            ) {\n                                priority++;\n                            } else {\n                                priority = -1;\n                                break;\n                            }\n                        }\n                    }\n                    if (priority >= 0) {\n                        matchedRules.push([priority, entry[1]]);\n                    }\n                }\n\n                // Create the final rule by merging found rules by order of\n                // priority\n                const finalRule = {};\n                for (let p = 0; p <= keys.length; p++) {\n                    for (const entry of matchedRules) {\n                        if (entry[0] === p) {\n                            Object.assign(finalRule, entry[1]);\n                        }\n                    }\n                }\n\n                // Create an unique identifier for the set of values\n                // direction - state 1 - state2 to add the rule in the map\n                const hashCode = restoreStateRuleHashCode(direction, cType1, cType2);\n                map.set(hashCode, finalRule);\n            }\n        }\n    }\n\n    return map;\n})();\n/**\n * Restores the given state starting before the given while looking in the given\n * direction.\n *\n * @param {Object} prevStateData @see getState\n * @param {boolean} debug=false - if true, adds nicely formatted\n *     console logs to help with debugging.\n * @returns {Object|undefined} the rule that was applied to restore the state,\n *     if any, for testing purposes.\n */\nexport function restoreState(prevStateData, debug = false) {\n    const { node, direction, cType: cType1, oldEditableHTML } = prevStateData;\n    if (!node || !node.parentNode) {\n        // FIXME sometimes we want to restore the state starting from a node\n        // which has been removed by another restoreState call... Not sure if\n        // it is a problem or not, to investigate.\n        return;\n    }\n    const [el, offset] = direction === DIRECTIONS.LEFT ? leftPos(node) : rightPos(node);\n    const { cType: cType2 } = getState(el, offset, direction);\n\n    /**\n     * Knowing the old state data and the new state data, we know if we have to\n     * do something or not, and what to do.\n     */\n    const ruleHashCode = restoreStateRuleHashCode(direction, cType1, cType2);\n    const rule = allRestoreStateRules.get(ruleHashCode);\n    if (debug) {\n        const editable = closestElement(node, \".odoo-editor-editable\");\n        console.log(\n            \"%c\" +\n                node.textContent.replaceAll(\" \", \"_\").replaceAll(\"\\u200B\", \"ZWS\") +\n                \"\\n\" +\n                \"%c\" +\n                (direction === DIRECTIONS.LEFT ? \"left\" : \"right\") +\n                \"\\n\" +\n                \"%c\" +\n                ctypeToString(cType1) +\n                \"\\n\" +\n                \"%c\" +\n                ctypeToString(cType2) +\n                \"\\n\" +\n                \"%c\" +\n                \"BEFORE: \" +\n                (oldEditableHTML || \"(unavailable)\") +\n                \"\\n\" +\n                \"%c\" +\n                \"AFTER:  \" +\n                (editable\n                    ? editable.innerHTML.replaceAll(\" \", \"_\").replaceAll(\"\\u200B\", \"ZWS\")\n                    : \"(unavailable)\") +\n                \"\\n\",\n            \"color: white; display: block; width: 100%;\",\n            \"color: \" +\n                (direction === DIRECTIONS.LEFT ? \"magenta\" : \"lightgreen\") +\n                \"; display: block; width: 100%;\",\n            \"color: pink; display: block; width: 100%;\",\n            \"color: lightblue; display: block; width: 100%;\",\n            \"color: white; display: block; width: 100%;\",\n            \"color: white; display: block; width: 100%;\",\n            rule\n        );\n    }\n    if (Object.values(rule).filter((x) => x !== undefined).length) {\n        const inverseDirection = direction === DIRECTIONS.LEFT ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n        enforceWhitespace(el, offset, inverseDirection, rule);\n    }\n    return rule;\n}\n\n/**\n * Returns whether or not the given node is a BR element which does not really\n * act as a line break, but as a placeholder for the cursor or to make some left\n * element (like a space) visible.\n * @todo @phoenix this depends on state, so hard to move it to dom_info\n *\n * @param {HTMLBRElement} brEl\n * @returns {boolean}\n */\nexport function isFakeLineBreak(brEl) {\n    return !(getState(...rightPos(brEl), DIRECTIONS.RIGHT).cType & (CTYPES.CONTENT | CTGROUPS.BR));\n}\n\n/**\n * Enforces the whitespace and BR visibility in the given direction starting\n * from the given position.\n *\n * @param {HTMLElement} el\n * @param {number} offset\n * @param {number} direction @see DIRECTIONS.LEFT @see DIRECTIONS.RIGHT\n * @param {Object} rule\n * @param {boolean} [rule.spaceVisibility]\n * @param {boolean} [rule.brVisibility]\n */\nexport function enforceWhitespace(el, offset, direction, rule) {\n    const document = el.ownerDocument;\n    let domPath, whitespaceAtEdgeRegex;\n    if (direction === DIRECTIONS.LEFT) {\n        domPath = leftLeafOnlyNotBlockPath(el, offset);\n        whitespaceAtEdgeRegex = new RegExp(whitespace + \"+$\");\n    } else {\n        domPath = rightLeafOnlyNotBlockPath(el, offset);\n        whitespaceAtEdgeRegex = new RegExp(\"^\" + whitespace + \"+\");\n    }\n\n    const invisibleSpaceTextNodes = [];\n    let foundVisibleSpaceTextNode = null;\n    for (const node of domPath) {\n        if (node.nodeName === \"BR\") {\n            if (rule.brVisibility === undefined) {\n                break;\n            }\n            if (rule.brVisibility) {\n                node.before(document.createElement(\"br\"));\n            } else {\n                if (!rule.extraBRRemovalCondition || rule.extraBRRemovalCondition(node)) {\n                    node.remove();\n                }\n            }\n            break;\n        } else if (node.nodeType === Node.TEXT_NODE && !isInPre(node)) {\n            if (whitespaceAtEdgeRegex.test(node.nodeValue)) {\n                // If we hit spaces going in the direction, either they are in a\n                // visible text node and we have to change the visibility of\n                // those spaces, or it is in an invisible text node. In that\n                // last case, we either remove the spaces if there are spaces in\n                // a visible text node going further in the direction or we\n                // change the visiblity or those spaces.\n                if (!isWhitespace(node)) {\n                    foundVisibleSpaceTextNode = node;\n                    break;\n                } else {\n                    invisibleSpaceTextNodes.push(node);\n                }\n            } else if (!isWhitespace(node)) {\n                break;\n            }\n        } else {\n            break;\n        }\n    }\n\n    if (rule.spaceVisibility === undefined) {\n        return;\n    }\n    if (!rule.spaceVisibility) {\n        for (const node of invisibleSpaceTextNodes) {\n            // Empty and not remove to not mess with offset-based positions in\n            // commands implementation, also remove non-block empty parents.\n            node.nodeValue = \"\";\n            const ancestorPath = closestPath(node.parentNode);\n            let toRemove = null;\n            for (const pNode of ancestorPath) {\n                if (toRemove) {\n                    toRemove.remove();\n                }\n                if (pNode.childNodes.length === 1 && !isBlock(pNode)) {\n                    pNode.after(node);\n                    toRemove = pNode;\n                } else {\n                    break;\n                }\n            }\n        }\n    }\n    const spaceNode = foundVisibleSpaceTextNode || invisibleSpaceTextNodes[0];\n    if (spaceNode) {\n        let spaceVisibility = rule.spaceVisibility;\n        // In case we are asked to replace the space by a &nbsp;, disobey and\n        // do the opposite if that space is currently not visible\n        // TODO I'd like this to not be needed, it feels wrong...\n        if (\n            spaceVisibility &&\n            !foundVisibleSpaceTextNode &&\n            getState(...rightPos(spaceNode), DIRECTIONS.RIGHT).cType & CTGROUPS.BLOCK &&\n            getState(...leftPos(spaceNode), DIRECTIONS.LEFT).cType !== CTYPES.CONTENT\n        ) {\n            spaceVisibility = false;\n        }\n        spaceNode.nodeValue = spaceNode.nodeValue.replace(\n            whitespaceAtEdgeRegex,\n            spaceVisibility ? \"\\u00A0\" : \"\"\n        );\n    }\n}\n", "import { DIRECTIONS } from \"./position\";\n\nexport const closestPath = function* (node) {\n    while (node) {\n        yield node;\n        node = node.parentNode;\n    }\n};\n\n/**\n * Find a node.\n * @param {findCallback} findCallback - This callback check if this function\n *      should return `node`.\n * @param {findCallback} stopCallback - This callback check if this function\n *      should stop when it receive `node`.\n */\nexport function findNode(domPath, findCallback = () => true, stopCallback = () => false) {\n    for (const node of domPath) {\n        if (findCallback(node)) {\n            return node;\n        }\n        if (stopCallback(node)) {\n            break;\n        }\n    }\n    return null;\n}\n\n/**\n * @param {Node} node\n * @param {HTMLElement} limitAncestor - non inclusive limit ancestor to search for\n * @param {Function} predicate\n * @returns {Node|null}\n */\nexport function findUpTo(node, limitAncestor, predicate) {\n    while (node !== limitAncestor) {\n        if (predicate(node)) {\n            return node;\n        }\n        node = node.parentElement;\n    }\n    return null;\n}\n\n/**\n * @param {Node} node\n * @param {HTMLElement} limitAncestor - non inclusive limit ancestor to search for\n * @param {Function} predicate\n * @returns {Node|undefined}\n */\nexport function findFurthest(node, limitAncestor, predicate) {\n    const nodes = [];\n    while (node !== limitAncestor) {\n        nodes.push(node);\n        node = node.parentNode;\n    }\n    return nodes.findLast(predicate);\n}\n\n/**\n * Returns the closest HTMLElement of the provided Node. If the predicate is a\n * string, returns the closest HTMLElement that match the predicate selector. If\n * the predicate is a function, returns the closest element that matches the\n * predicate. Any returned element will be contained within the editable, or is\n * disconnected from any Document.\n *\n * Rationale: this helper is used to manipulate editor nodes, and should never\n * match any node outside of that scope. Disconnected nodes are assumed to be\n * from the editor, since they are likely removed nodes evaluated in the context\n * of the MutationObserver handler @see ProtectedNodePlugin\n *\n * @param {Node} node\n * @param {string | Function} [predicate='*']\n * @returns {HTMLElement|null}\n */\nexport function closestElement(node, predicate = \"*\") {\n    let element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;\n    const editable = element?.closest(\".odoo-editor-editable\");\n    if (typeof predicate === \"function\") {\n        while (element && !predicate(element)) {\n            element = element.parentElement;\n        }\n    } else {\n        element = element?.closest(predicate);\n    }\n    if ((editable && editable.contains(element)) || !node.isConnected) {\n        return element;\n    }\n    return null;\n}\n\n/**\n * Returns a list of all the ancestors nodes of the provided node.\n *\n * @param {Node} node\n * @param {Node} [editable] include to prevent bubbling up further than the editable.\n * @returns {HTMLElement[]}\n */\nexport function ancestors(node, editable) {\n    const result = [];\n    while (node && node.parentElement && node !== editable) {\n        result.push(node.parentElement);\n        node = node.parentElement;\n    }\n    return result;\n}\n\n/**\n * Get a static array of children, to avoid manipulating the live HTMLCollection\n * for better performances.\n *\n * @param {Element}} elem\n * @returns {Array<Element>} children\n */\nexport function children(elem) {\n    const children = [];\n    let child = elem.firstElementChild;\n    while (child) {\n        children.push(child);\n        child = child.nextElementSibling;\n    }\n    return children;\n}\n\n/**\n * Get a static array of childNodes, to avoid manipulating the live NodeList for\n * better performances.\n *\n * @param {Node}} node\n * @returns {Array<Node>} childNodes\n */\nexport function childNodes(node) {\n    const childNodes = [];\n    let child = node.firstChild;\n    while (child) {\n        childNodes.push(child);\n        child = child.nextSibling;\n    }\n    return childNodes;\n}\n\n/**\n * Take a node, return all of its descendants, in depth-first order.\n *\n * @param {Node} node\n * @returns {Node[]}\n */\nexport function descendants(node, posterity = []) {\n    let child = node.firstChild;\n    while (child) {\n        posterity.push(child);\n        descendants(child, posterity);\n        child = child.nextSibling;\n    }\n    return posterity;\n}\n\n/**\n * Values which can be returned while browsing the DOM which gives information\n * to why the path ended.\n */\nexport const PATH_END_REASONS = {\n    NO_NODE: 0,\n    BLOCK_OUT: 1,\n    BLOCK_HIT: 2,\n    OUT_OF_SCOPE: 3,\n};\n\n/**\n * Creates a generator function according to the given parameters. Pre-made\n * generators to traverse the DOM are made using this function:\n *\n * @see leftLeafFirstPath\n * @see leftLeafOnlyNotBlockPath\n * @see leftLeafOnlyInScopeNotBlockEditablePath\n * @see rightLeafOnlyNotBlockPath\n * @see rightLeafOnlyNotBlockNotEditablePath\n *\n * @param {boolean} direction\n * @param {Object} options\n * @param {boolean} [options.leafOnly] if true, do not yield any non-leaf node\n * @param {boolean} [options.inScope] if true, stop the generator as soon as a node is not\n *                      a descendant of `node` provided when traversing the\n *                      generated function.\n * @param {Function} [options.stopTraverseFunction] a function that takes a node\n *                      and should return true when a node descendant should not\n *                      be traversed.\n * @param {Function} [options.stopFunction] function that makes the generator stop when a\n *                      node is encountered.\n */\nexport function createDOMPathGenerator(\n    direction,\n    { leafOnly = false, inScope = false, stopTraverseFunction, stopFunction } = {}\n) {\n    const nextDeepest =\n        direction === DIRECTIONS.LEFT\n            ? (node) => lastLeaf(node.previousSibling, stopTraverseFunction)\n            : (node) => firstLeaf(node.nextSibling, stopTraverseFunction);\n\n    const firstNode =\n        direction === DIRECTIONS.LEFT\n            ? (node, offset) => lastLeaf(node.childNodes[offset - 1], stopTraverseFunction)\n            : (node, offset) => firstLeaf(node.childNodes[offset], stopTraverseFunction);\n\n    // Note \"reasons\" is a way for the caller to be able to know why the\n    // generator ended yielding values.\n    return function* (node, offset, reasons = []) {\n        let movedUp = false;\n\n        let currentNode = firstNode(node, offset);\n        if (!currentNode) {\n            movedUp = true;\n            currentNode = node;\n        }\n\n        while (currentNode) {\n            if (stopFunction && stopFunction(currentNode)) {\n                reasons.push(movedUp ? PATH_END_REASONS.BLOCK_OUT : PATH_END_REASONS.BLOCK_HIT);\n                break;\n            }\n            if (inScope && currentNode === node) {\n                reasons.push(PATH_END_REASONS.OUT_OF_SCOPE);\n                break;\n            }\n            if (!(leafOnly && movedUp)) {\n                yield currentNode;\n            }\n\n            movedUp = false;\n            let nextNode = nextDeepest(currentNode);\n            if (!nextNode) {\n                movedUp = true;\n                nextNode = currentNode.parentNode;\n            }\n            currentNode = nextNode;\n        }\n\n        reasons.push(PATH_END_REASONS.NO_NODE);\n    };\n}\n\n/**\n * Returns the deepest child in last position.\n *\n * @param {Node} node\n * @param {Function} [stopTraverseFunction]\n * @returns {Node}\n */\nexport function lastLeaf(node, stopTraverseFunction) {\n    while (node && node.lastChild && !(stopTraverseFunction && stopTraverseFunction(node))) {\n        node = node.lastChild;\n    }\n    return node;\n}\n/**\n * Returns the deepest child in first position.\n *\n * @param {Node} node\n * @param {Function} [stopTraverseFunction]\n * @returns {Node}\n */\nexport function firstLeaf(node, stopTraverseFunction) {\n    while (node && node.firstChild && !(stopTraverseFunction && stopTraverseFunction(node))) {\n        node = node.firstChild;\n    }\n    return node;\n}\n\n/**\n * Returns all the previous siblings of the given node until the first\n * sibling that does not satisfy the predicate, in lookup order.\n *\n * @param {Node} node\n * @param {Function} [predicate] (node: Node) => boolean\n */\nexport function getAdjacentPreviousSiblings(node, predicate = (n) => !!n) {\n    let previous = node.previousSibling;\n    const list = [];\n    while (previous && predicate(previous)) {\n        list.push(previous);\n        previous = previous.previousSibling;\n    }\n    return list;\n}\n/**\n * Returns all the next siblings of the given node until the first\n * sibling that does not satisfy the predicate, in lookup order.\n *\n * @param {Node} node\n * @param {Function} [predicate] (node: Node) => boolean\n */\nexport function getAdjacentNextSiblings(node, predicate = (n) => !!n) {\n    let next = node.nextSibling;\n    const list = [];\n    while (next && predicate(next)) {\n        list.push(next);\n        next = next.nextSibling;\n    }\n    return list;\n}\n/**\n * Returns all the adjacent siblings of the given node until the first sibling\n * (in both directions) that does not satisfy the predicate, in index order. If\n * the given node does not satisfy the predicate, an empty array is returned.\n *\n * @param {Node} node\n * @param {Function} [predicate] (node: Node) => boolean\n */\nexport function getAdjacents(node, predicate = (n) => !!n) {\n    const previous = getAdjacentPreviousSiblings(node, predicate);\n    const next = getAdjacentNextSiblings(node, predicate);\n    return predicate(node) ? [...previous.reverse(), node, ...next] : [];\n}\n\n/**\n * Returns the deepest common ancestor element of the given nodes within the\n * specified root element. If no root element is provided, the entire document\n * is considered as the root.\n *\n * @param {Node[]} nodes - The nodes for which to find the common ancestor.\n * @param {Element} [root] - The root element within which to search for the common ancestor.\n * @returns {Element|null} - The common ancestor element, or null if no common ancestor is found.\n */\nexport function getCommonAncestor(nodes, root = undefined) {\n    const pathsToRoot = nodes.map((node) => [node, ...ancestors(node, root)]);\n\n    let candidate = pathsToRoot[0]?.at(-1);\n    if (root && candidate !== root) {\n        return null;\n    }\n    let commonAncestor = null;\n    while (candidate && pathsToRoot.every((path) => path.at(-1) === candidate)) {\n        commonAncestor = candidate;\n        pathsToRoot.forEach((path) => path.pop());\n        candidate = pathsToRoot[0].at(-1);\n    }\n    return commonAncestor;\n}\n\n/**\n * Basically a wrapper around `root.querySelectorAll` that includes the\n * root.\n *\n * @param {Element} root\n * @param {string} selector\n * @returns {Generator<Element>}\n */\nexport const selectElements = function* (root, selector) {\n    if (root.matches(selector)) {\n        yield root;\n    }\n    for (const elem of root.querySelectorAll(selector)) {\n        yield elem;\n    }\n};\n", "import { makeDraggableHook } from \"@web/core/utils/draggable_hook_builder\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { reactive } from \"@odoo/owl\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { closest, touching } from \"@web/core/utils/ui\";\n\n/** @typedef {import(\"@web/core/utils/draggable_hook_builder\").DraggableHandlerParams} DraggableHandlerParams */\n/** @typedef {import(\"@web/core/utils/draggable_hook_builder\").DraggableBuilderParams} DraggableBuilderParams */\n/** @typedef {import(\"@web/core/utils/draggable\").DraggableParams} DraggableParams */\n\n/** @typedef {DraggableHandlerParams & { dropzone: HTMLElement | null, helper: HTMLElement }} DragAndDropHandlerParams */\n/** @typedef {DraggableHandlerParams & { helper: HTMLElement }} DragAndDropStartParams */\n/** @typedef {DraggableHandlerParams & { dropzone: HTMLElement }} DropzoneHandlerParams */\n/**\n * @typedef DragAndDropParams\n * @extends {DraggableParams}\n *\n * MANDATORY\n * @property {(() => Array)} dropzones a function that returns the available dropzones\n * @property {(() => HTMLElement)} helper a function that returns a helper element\n * that will follow the cursor when dragging\n * @property {HTMLElement || (() => HTMLElement)} scrollingElement the element on\n * which a scroll should be triggered\n *\n * HANDLERS (Optional)\n * @property {(params: DragAndDropStartParams) => any} [onDragStart]\n * called when a dragging sequence is initiated\n * @property {(params: DropzoneHandlerParams) => any} [dropzoneOver]\n * called when an element is over a dropzone\n * @property {(params: DropzoneHandlerParams) => any} [dropzoneOut]\n * called when an element is leaving a dropzone\n * @property {(params: DragAndDropHandlerParams) => any} [onDrag]\n * called when an element is being dragged\n * @property {(params: DragAndDropHandlerParams) => any} [onDragEnd]\n * called when the dragging sequence is over\n */\n/**\n * @typedef NativeDraggableState\n * @property {(params: DraggableParams) => any} update\n * method to update the params of the draggable\n * @property {import(\"@web/core/utils/draggable\").DraggableState} state\n * state of the draggable component\n * @property {() => any} destroy\n * method to destroy and unbind the draggable component\n */\n/**\n * Utility function to create a native draggable component\n *\n * @param {DraggableBuilderParams} hookParams\n * @param {DraggableParams} initialParams\n * @returns {NativeDraggableState}\n */\nexport function useNativeDraggable(hookParams, initialParams) {\n    const setupFunctions = new Map();\n    const cleanupFunctions = [];\n    const currentParams = { ...initialParams };\n    const setupHooks = {\n        wrapState: reactive,\n        throttle: throttleForAnimation,\n        addListener: (el, type, callback, options) => {\n            el.addEventListener(type, callback, options);\n            cleanupFunctions.push(() => el.removeEventListener(type, callback));\n        },\n        setup: (setupFn, depsFn) => setupFunctions.set(setupFn, depsFn),\n        teardown: (cleanupFn) => {\n            cleanupFunctions.push(cleanupFn);\n        },\n    };\n    // Compatibility for tests\n    const el = initialParams.ref.el;\n    // TODO this is probably to be removed in master: the received params\n    // contain the selector that should be checked and it will be transferred\n    // to the makeDraggableHook function. There should not be any need to add\n    // the default selector class here.\n    el.classList.add(\"o_draggable\");\n    cleanupFunctions.push(() => el.classList.remove(\"o_draggable\"));\n\n    const draggableState = makeDraggableHook({ setupHooks, ...hookParams })(currentParams);\n    draggableState.enable = true;\n    const draggableComponent = {\n        state: draggableState,\n        update: (newParams) => {\n            Object.assign(currentParams, newParams);\n            setupFunctions.forEach((depsFn, setupFn) => setupFn(...depsFn()));\n        },\n        destroy: () => {\n            cleanupFunctions.forEach((cleanupFn) => cleanupFn());\n        },\n    };\n    draggableComponent.update({});\n    return draggableComponent;\n}\n\nfunction updateElementPosition(el, { x, y }, styleFn, offset = { x: 0, y: 0 }) {\n    return styleFn(el, { top: `${y - offset.y}px`, left: `${x - offset.x}px` });\n}\n/** @type DraggableBuilderParams */\nconst dragAndDropHookParams = {\n    name: \"useDragAndDrop\",\n    acceptedParams: {\n        dropzones: [Function],\n        scrollingElement: [Object, Function],\n        helper: [Function],\n        extraWindow: [Object, Function],\n    },\n    edgeScrolling: { enabled: true },\n    onComputeParams({ ctx, params }) {\n        // The helper is mandatory and will follow the cursor instead\n        ctx.followCursor = false;\n        ctx.scrollingElement = params.scrollingElement;\n        ctx.getHelper = params.helper;\n        ctx.getDropZones = params.dropzones;\n    },\n    onWillStartDrag: ({ ctx }) => {\n        ctx.current.container = ctx.scrollingElement;\n        ctx.current.helperOffset = { x: 0, y: 0 };\n    },\n    onDragStart: ({ ctx, addStyle, addCleanup }) => {\n        // Use the helper as the tracking element to properly update scroll values.\n        ctx.current.element = ctx.getHelper({ ...ctx.current, ...ctx.pointer });\n        ctx.current.helper = ctx.current.element;\n        ctx.current.helper.style.position = \"fixed\";\n        // We want the pointer events on the helper so that the cursor\n        // is properly displayed.\n        ctx.current.helper.classList.remove(\"o_dragged\");\n        ctx.current.helper.style.cursor = ctx.cursor;\n        ctx.current.helper.style.pointerEvents = \"auto\";\n\n        // If the helper is inside the iframe, we want pointer events on the\n        // frame element so that they reach the window and properly apply\n        // the cursor.\n        const frameElement = ctx.current.helper.ownerDocument.defaultView.frameElement;\n        if (frameElement) {\n            addStyle(frameElement, { pointerEvents: \"auto\" });\n        }\n\n        addCleanup(() => ctx.current.helper.remove());\n\n        updateElementPosition(ctx.current.helper, ctx.pointer, addStyle, ctx.current.helperOffset);\n\n        return pick(ctx.current, \"element\", \"helper\");\n    },\n    onDrag: ({ ctx, addStyle, callHandler }) => {\n        ctx.current.helper.classList.add(\"o_draggable_dragging\");\n\n        updateElementPosition(ctx.current.helper, ctx.pointer, addStyle, ctx.current.helperOffset);\n        // Unfortunately, DOMRect is not an Object, so spreading operator from\n        // `touching` does not work, so convert DOMRect to plain object.\n        let helperRect = ctx.current.helper.getBoundingClientRect();\n        helperRect = {\n            x: helperRect.x,\n            y: helperRect.y,\n            width: helperRect.width,\n            height: helperRect.height,\n        };\n        const dropzoneEl = closest(touching(ctx.getDropZones(), helperRect), helperRect);\n        // Update the drop zone if it's in grid mode\n        if (\n            ctx.current.dropzone?.el &&\n            ctx.current.dropzone.el.classList.contains(\"oe_grid_zone\")\n        ) {\n            ctx.current.dropzone.rect = ctx.current.dropzone.el.getBoundingClientRect();\n        }\n        if (\n            ctx.current.dropzone &&\n            (ctx.current.dropzone.el === dropzoneEl ||\n                (!dropzoneEl &&\n                    touching([ctx.current.helper], ctx.current.dropzone.rect).length > 0))\n        ) {\n            // If no new dropzone but old one is still valid, return early.\n            return pick(ctx.current, \"element\", \"dropzone\", \"helper\");\n        }\n\n        if (ctx.current.dropzone && dropzoneEl !== ctx.current.dropzone.el) {\n            callHandler(\"dropzoneOut\", { dropzone: ctx.current.dropzone });\n            delete ctx.current.dropzone;\n        }\n\n        if (dropzoneEl) {\n            // Save rect information prior to calling the over function\n            // to keep a consistent dropzone even if content was added.\n            const rect = DOMRect.fromRect(dropzoneEl.getBoundingClientRect());\n            ctx.current.dropzone = {\n                el: dropzoneEl,\n                rect: {\n                    x: rect.x,\n                    y: rect.y,\n                    width: rect.width,\n                    height: rect.height,\n                },\n            };\n            callHandler(\"dropzoneOver\", { dropzone: ctx.current.dropzone });\n        }\n        return pick(ctx.current, \"element\", \"dropzone\", \"helper\");\n    },\n    onDragEnd({ ctx }) {\n        return pick(ctx.current, \"element\", \"dropzone\", \"helper\");\n    },\n};\n/**\n * Function to start a drag and drop handler\n *\n * @param {DragAndDropParams} initialParams params given to the drag and drop\n * component\n * @returns {NativeDraggableState}\n */\nexport function useDragAndDrop(initialParams) {\n    return useNativeDraggable(dragAndDropHookParams, initialParams);\n}\n", "export const fonts = {\n    /**\n     * Retrieves all the CSS rules which match the given parser (Regex).\n     *\n     * @param {Regex} filter\n     * @returns {Object[]} Array of CSS rules descriptions (objects). A rule is\n     *          defined by 3 values: 'selector', 'css' and 'names'. 'selector'\n     *          is a string which contains the whole selector, 'css' is a string\n     *          which contains the css properties and 'names' is an array of the\n     *          first captured groups for each selector part. E.g.: if the\n     *          filter is set to match .fa-* rules and capture the icon names,\n     *          the rule:\n     *              '.fa-alias1::before, .fa-alias2::before { hello: world; }'\n     *          will be retrieved as\n     *              {\n     *                  selector: '.fa-alias1::before, .fa-alias2::before',\n     *                  css: 'hello: world;',\n     *                  names: ['.fa-alias1', '.fa-alias2'],\n     *              }\n     */\n    cacheCssSelectors: {},\n    getCssSelectors: function (filter) {\n        if (this.cacheCssSelectors[filter]) {\n            return this.cacheCssSelectors[filter];\n        }\n        this.cacheCssSelectors[filter] = [];\n        var sheets = document.styleSheets;\n        for (var i = 0; i < sheets.length; i++) {\n            var rules;\n            try {\n                // try...catch because Firefox not able to enumerate\n                // document.styleSheets[].cssRules[] for cross-domain\n                // stylesheets.\n                rules = sheets[i].rules || sheets[i].cssRules;\n            } catch {\n                continue;\n            }\n            if (!rules) {\n                continue;\n            }\n\n            for (var r = 0; r < rules.length; r++) {\n                var selectorText = rules[r].selectorText;\n                if (!selectorText) {\n                    continue;\n                }\n                var selectors = selectorText.split(/\\s*,\\s*/);\n                var data = null;\n                for (var s = 0; s < selectors.length; s++) {\n                    var match = selectors[s].trim().match(filter);\n                    if (!match) {\n                        continue;\n                    }\n                    if (!data) {\n                        data = {\n                            selector: match[0],\n                            css: rules[r].cssText.replace(/(^.*\\{\\s*)|(\\s*\\}\\s*$)/g, \"\"),\n                            names: [match[1]],\n                        };\n                    } else {\n                        data.selector += \", \" + match[0];\n                        data.names.push(match[1]);\n                    }\n                }\n                if (data) {\n                    this.cacheCssSelectors[filter].push(data);\n                }\n            }\n        }\n        return this.cacheCssSelectors[filter];\n    },\n    /**\n     * List of font icons to load by editor. The icons are displayed in the media\n     * editor and identified like font and image (can be colored, spinned, resized\n     * with fa classes).\n     * To add font, push a new object {base, parser}\n     *\n     * - base: class who appear on all fonts\n     * - parser: regular expression used to select all font in css stylesheets\n     *\n     * @type Array\n     */\n    fontIcons: [{ base: \"fa\", parser: /\\.(fa-(?:\\w|-)+)::?before/i }],\n    computedFonts: false,\n    /**\n     * Searches the fonts described by the @see fontIcons variable.\n     */\n    computeFonts: function () {\n        if (!this.computedFonts) {\n            var self = this;\n            this.fontIcons.forEach((data) => {\n                data.cssData = self.getCssSelectors(data.parser);\n                data.alias = data.cssData.map((x) => x.names).flat();\n            });\n            this.computedFonts = true;\n        }\n    },\n};\n", "import { normalizeCSSColor } from \"@web/core/utils/colors\";\nimport { removeClass } from \"./dom\";\nimport { isBold, isDirectionSwitched, isItalic, isStrikeThrough, isUnderline } from \"./dom_info\";\nimport { closestElement } from \"./dom_traversal\";\n\n/**\n * Array of all the classes used by the editor to change the font size.\n */\nexport const FONT_SIZE_CLASSES = [\n    \"display-1-fs\",\n    \"display-2-fs\",\n    \"display-3-fs\",\n    \"display-4-fs\",\n    \"h1-fs\",\n    \"h2-fs\",\n    \"h3-fs\",\n    \"h4-fs\",\n    \"h5-fs\",\n    \"h6-fs\",\n    \"base-fs\",\n    \"small\",\n];\n\nexport const TEXT_STYLE_CLASSES = [\"display-1\", \"display-2\", \"display-3\", \"display-4\", \"lead\"];\n\nexport const formatsSpecs = {\n    italic: {\n        tagName: \"em\",\n        isFormatted: isItalic,\n        isTag: (node) => [\"EM\", \"I\"].includes(node.tagName),\n        hasStyle: (node) => Boolean(node.style && node.style[\"font-style\"]),\n        addStyle: (node) => (node.style[\"font-style\"] = \"italic\"),\n        addNeutralStyle: (node) => (node.style[\"font-style\"] = \"normal\"),\n        removeStyle: (node) => removeStyle(node, \"font-style\"),\n    },\n    bold: {\n        tagName: \"strong\",\n        isFormatted: isBold,\n        isTag: (node) => [\"STRONG\", \"B\"].includes(node.tagName),\n        hasStyle: (node) => Boolean(node.style && node.style[\"font-weight\"]),\n        addStyle: (node) => (node.style[\"font-weight\"] = \"bolder\"),\n        addNeutralStyle: (node) => {\n            node.style[\"font-weight\"] = \"normal\";\n        },\n        removeStyle: (node) => removeStyle(node, \"font-weight\"),\n    },\n    underline: {\n        tagName: \"u\",\n        isFormatted: isUnderline,\n        isTag: (node) => node.tagName === \"U\",\n        hasStyle: (node) =>\n            node.style &&\n            (node.style[\"text-decoration\"].includes(\"underline\") ||\n                node.style[\"text-decoration-line\"].includes(\"underline\")),\n        addStyle: (node) => (node.style[\"text-decoration-line\"] += \" underline\"),\n        removeStyle: (node) =>\n            removeStyle(\n                node,\n                node.style[\"text-decoration\"].includes(\"underline\")\n                    ? \"text-decoration\"\n                    : \"text-decoration-line\",\n                \"underline\"\n            ),\n    },\n    strikeThrough: {\n        tagName: \"s\",\n        isFormatted: isStrikeThrough,\n        isTag: (node) => node.tagName === \"S\",\n        hasStyle: (node) =>\n            node.style &&\n            (node.style[\"text-decoration\"].includes(\"line-through\") ||\n                node.style[\"text-decoration-line\"].includes(\"line-through\")),\n        addStyle: (node) => (node.style[\"text-decoration-line\"] += \" line-through\"),\n        removeStyle: (node) =>\n            removeStyle(\n                node,\n                node.style[\"text-decoration\"].includes(\"line-through\")\n                    ? \"text-decoration\"\n                    : \"text-decoration-line\",\n                \"line-through\"\n            ),\n    },\n    fontSize: {\n        isFormatted: (node) => node.style && node.style[\"font-size\"],\n        hasStyle: (node) => node.style && node.style[\"font-size\"],\n        addStyle: (node, props) => {\n            node.style[\"font-size\"] = props.size;\n            removeClass(node, ...FONT_SIZE_CLASSES);\n        },\n        removeStyle: (node) => removeStyle(node, \"font-size\"),\n    },\n    setFontSizeClassName: {\n        isFormatted: (node) => FONT_SIZE_CLASSES.find((cls) => node?.classList?.contains(cls)),\n        hasStyle: (node, props) => FONT_SIZE_CLASSES.find((cls) => node.classList.contains(cls)),\n        addStyle: (node, props) => node.classList.add(props.className),\n        removeStyle: (node) => removeClass(node, ...FONT_SIZE_CLASSES, ...TEXT_STYLE_CLASSES),\n    },\n    switchDirection: {\n        isFormatted: isDirectionSwitched,\n    },\n};\n\nfunction removeStyle(node, styleName, item) {\n    if (item) {\n        const newStyle = node.style[styleName]\n            .split(\" \")\n            .filter((x) => x !== item)\n            .join(\" \");\n        node.style[styleName] = newStyle || null;\n    } else {\n        node.style[styleName] = null;\n    }\n    if (node.getAttribute(\"style\") === \"\") {\n        node.removeAttribute(\"style\");\n    }\n}\n\n/**\n * @param {string} key\n * @param {object} htmlStyle\n * @returns {string}\n */\nexport function getCSSVariableValue(key, htmlStyle) {\n    // Get trimmed value from the HTML element\n    let value = htmlStyle.getPropertyValue(`--${key}`).trim();\n    // If it is a color value, it needs to be normalized\n    value = normalizeCSSColor(value);\n    // Normally scss-string values are \"printed\" single-quoted. That way no\n    // magic conversation is needed when customizing a variable: either save it\n    // quoted for strings or non quoted for colors, numbers, etc. However,\n    // Chrome has the annoying behavior of changing the single-quotes to\n    // double-quotes when reading them through getPropertyValue...\n    return value.replace(/\"/g, \"'\");\n}\n\n/**\n * Key-value mapping to list converters from an unit A to an unit B.\n * - The key is a string in the format '$1-$2' where $1 is the CSS symbol of\n *   unit A and $2 is the CSS symbol of unit B.\n * - The value is a function that converts the received value (expressed in\n *   unit A) to another value expressed in unit B. Two other parameters is\n *   received: the css property on which the unit applies and the jQuery element\n *   on which that css property may change.\n */\nconst CSS_UNITS_CONVERSION = {\n    \"s-ms\": () => 1000,\n    \"ms-s\": () => 0.001,\n    \"rem-px\": (htmlStyle) => parseFloat(htmlStyle[\"font-size\"]),\n    \"px-rem\": (htmlStyle) => 1 / parseFloat(htmlStyle[\"font-size\"]),\n    \"%-px\": () => -1, // Not implemented but should simply be ignored for now\n    \"px-%\": () => -1, // Not implemented but should simply be ignored for now\n};\n\n/**\n * Converts the given numeric value expressed in the given css unit into\n * the corresponding numeric value expressed in the other given css unit.\n *\n * e.g. fct(400, 'ms', 's') -> 0.4\n *\n * @param {number} value\n * @param {string} unitFrom\n * @param {string} unitTo\n * @param {object} htmlStyle\n * @returns {number}\n */\nexport function convertNumericToUnit(value, unitFrom, unitTo, htmlStyle) {\n    if (Math.abs(value) < Number.EPSILON || unitFrom === unitTo) {\n        return value;\n    }\n    const converter = CSS_UNITS_CONVERSION[`${unitFrom}-${unitTo}`];\n    if (converter === undefined) {\n        throw new Error(`Cannot convert '${unitFrom}' units into '${unitTo}' units !`);\n    }\n    return value * converter(htmlStyle);\n}\n\nexport function getHtmlStyle(document) {\n    return document.defaultView.getComputedStyle(document.documentElement);\n}\n\n/**\n * Finds the font size to display for the current selection. We cannot rely\n * on the computed font-size only as font-sizes are responsive and we always\n * want to display the desktop (integer when possible) one.\n *\n * @param {Selection} sel The current selection.\n * @param {Document} document The document of the current selection.\n * @returns {Float} The font size to display.\n */\nexport function getFontSizeDisplayValue(sel, document) {\n    const tagNameRelatedToFontSize = [\"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\"];\n    const styleClassesRelatedToFontSize = [\"display-1\", \"display-2\", \"display-3\", \"display-4\"];\n    const closestStartContainerEl = closestElement(sel.startContainer);\n    const closestFontSizedEl = closestStartContainerEl.closest(`\n        [style*='font-size'],\n        ${FONT_SIZE_CLASSES.map((className) => `.${className}`)},\n        ${styleClassesRelatedToFontSize.map((className) => `.${className}`)},\n        ${tagNameRelatedToFontSize}\n    `);\n    let remValue;\n    const htmlStyle = getHtmlStyle(document);\n    if (closestFontSizedEl) {\n        const useFontSizeInput = closestFontSizedEl.style.fontSize;\n        if (useFontSizeInput) {\n            // Use the computed value to always convert to px. However, this\n            // currently does not check that the inline font-size is the one\n            // actually having an effect (there could be an !important CSS rule\n            // forcing something else).\n            // TODO align with the behavior of the rest of the editor snippet\n            // options.\n            return parseFloat(getComputedStyle(closestStartContainerEl).fontSize);\n        }\n        // It's a class font size or a hN tag. We don't return the computed\n        // font size because it can be different from the one displayed in\n        // the toolbar because it's responsive.\n        const fontSizeClass = FONT_SIZE_CLASSES.find((className) =>\n            closestFontSizedEl.classList.contains(className)\n        );\n        let fsName;\n        if (fontSizeClass) {\n            fsName = fontSizeClass.substring(0, fontSizeClass.length - 3); // Without -fs\n        } else {\n            fsName =\n                styleClassesRelatedToFontSize.find((className) =>\n                    closestFontSizedEl.classList.contains(className)\n                ) || closestFontSizedEl.tagName.toLowerCase();\n        }\n        remValue = parseFloat(getCSSVariableValue(`${fsName}-font-size`, htmlStyle));\n    }\n    // It's default font size (no font size class / style).\n    if (remValue === undefined) {\n        remValue = parseFloat(getCSSVariableValue(\"font-size-base\", htmlStyle));\n    }\n    const pxValue = convertNumericToUnit(remValue, \"rem\", \"px\", htmlStyle);\n    return pxValue || parseFloat(getComputedStyle(closestStartContainerEl).fontSize);\n}\n", "/**\n * @param { Document } document\n * @param { string } html\n * @returns { DocumentFragment }\n */\nexport function parseHTML(document, html) {\n    const fragment = document.createDocumentFragment();\n    const parser = new document.defaultView.DOMParser();\n    const parsedDocument = parser.parseFromString(html, \"text/html\");\n    fragment.replaceChildren(...parsedDocument.body.childNodes);\n    return fragment;\n}\n\n/**\n * Server-side, HTML is stored as a string which can have a different format\n * than what the current browser returns through outerHTML or innerHTML, notably\n * because of HTML entities.\n * This function can be used to convert strings with potential HTML entities to\n * the format used by the current browser. This allows comparisons between\n * values returned by the server and values extracted from the DOM using i.e.\n * innerHTML.\n *\n * @param { string } content\n * @param { function } cleanup receives the body element containing the parsed\n *        html, to perform some cleanup for the comparison.\n * @returns { string }\n */\nexport function normalizeHTML(content, cleanup = () => {}) {\n    const parser = new document.defaultView.DOMParser();\n    const body = parser.parseFromString(content, \"text/html\").body;\n    cleanup(body);\n    return body.innerHTML;\n}\n", "import { isColorGradient } from \"./color\";\n\n/**\n * Extracts url and gradient parts from the background-image CSS property.\n *\n * @param {string} CSS 'background-image' property value\n * @returns {Object} contains the separated 'url' and 'gradient' parts\n */\nexport function backgroundImageCssToParts(css) {\n    const parts = {};\n    css = css || \"\";\n    if (css.startsWith(\"url(\")) {\n        const urlEnd = css.indexOf(\")\") + 1;\n        parts.url = css.substring(0, urlEnd).trim();\n        const commaPos = css.indexOf(\",\", urlEnd);\n        css = commaPos > 0 ? css.substring(commaPos + 1) : \"\";\n    }\n    if (isColorGradient(css)) {\n        parts.gradient = css.trim();\n    }\n    return parts;\n}\n\n/**\n * Combines url and gradient parts into a background-image CSS property value\n *\n * @param {Object} contains the separated 'url' and 'gradient' parts\n * @returns {string} CSS 'background-image' property value\n */\nexport function backgroundImagePartsToCss(parts) {\n    let css = parts.url || \"\";\n    if (parts.gradient) {\n        css += (css ? \", \" : \"\") + parts.gradient;\n    }\n    return css || \"none\";\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { getAffineApproximation, getProjective } from \"./perspective_utils\";\n\n// Fields returned by cropperjs 'getData' method, also need to be passed when\n// initializing the cropper to reuse the previous crop.\nexport const cropperDataFields = [\"x\", \"y\", \"width\", \"height\", \"rotate\", \"scaleX\", \"scaleY\"];\nexport const isGif = (mimetype) => mimetype === \"image/gif\";\n\n// webgl color filters\nconst _applyAll = (result, filter, filters) => {\n    filters.forEach((f) => {\n        if (f[0] === \"blend\") {\n            const cv = f[1];\n            const ctx = result.getContext(\"2d\");\n            ctx.globalCompositeOperation = f[2];\n            ctx.globalAlpha = f[3];\n            ctx.drawImage(cv, 0, 0);\n            ctx.globalCompositeOperation = \"source-over\";\n            ctx.globalAlpha = 1.0;\n        } else {\n            filter.addFilter(...f);\n        }\n    });\n};\nlet applyAll;\n\nconst glFilters = {\n    blur: (filter) => filter.addFilter(\"blur\", 10),\n\n    1977: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        ctx.fillStyle = \"rgb(243, 106, 188)\";\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"screen\", 0.3],\n            [\"brightness\", 0.1],\n            [\"contrast\", 0.1],\n            [\"saturation\", 0.3],\n        ]);\n    },\n\n    aden: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        ctx.fillStyle = \"rgb(66, 10, 14)\";\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"darken\", 0.2],\n            [\"brightness\", 0.2],\n            [\"contrast\", -0.1],\n            [\"saturation\", -0.15],\n            [\"hue\", 20],\n        ]);\n    },\n\n    brannan: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        ctx.fillStyle = \"rgb(161, 44, 191)\";\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"lighten\", 0.31],\n            [\"sepia\", 0.5],\n            [\"contrast\", 0.4],\n        ]);\n    },\n\n    earlybird: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        const gradient = ctx.createRadialGradient(\n            cv.width / 2,\n            cv.height / 2,\n            0,\n            cv.width / 2,\n            cv.height / 2,\n            Math.hypot(cv.width, cv.height) / 2\n        );\n        gradient.addColorStop(0.2, \"#D0BA8E\");\n        gradient.addColorStop(1, \"#1D0210\");\n        ctx.fillStyle = gradient;\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"overlay\", 0.2],\n            [\"sepia\", 0.2],\n            [\"contrast\", -0.1],\n        ]);\n    },\n\n    inkwell: (filter, cv) => {\n        applyAll(filter, [\n            [\"sepia\", 0.3],\n            [\"brightness\", 0.1],\n            [\"contrast\", -0.1],\n            [\"desaturateLuminance\"],\n        ]);\n    },\n\n    // Needs hue blending mode for perfect reproduction. Close enough?\n    maven: (filter, cv) => {\n        applyAll(filter, [\n            [\"sepia\", 0.25],\n            [\"brightness\", -0.05],\n            [\"contrast\", -0.05],\n            [\"saturation\", 0.5],\n        ]);\n    },\n\n    toaster: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        const gradient = ctx.createRadialGradient(\n            cv.width / 2,\n            cv.height / 2,\n            0,\n            cv.width / 2,\n            cv.height / 2,\n            Math.hypot(cv.width, cv.height) / 2\n        );\n        gradient.addColorStop(0, \"#0F4E80\");\n        gradient.addColorStop(1, \"#3B003B\");\n        ctx.fillStyle = gradient;\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"screen\", 0.5],\n            [\"brightness\", -0.1],\n            [\"contrast\", 0.5],\n        ]);\n    },\n\n    walden: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        ctx.fillStyle = \"#CC4400\";\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"screen\", 0.3],\n            [\"sepia\", 0.3],\n            [\"brightness\", 0.1],\n            [\"saturation\", 0.6],\n            [\"hue\", 350],\n        ]);\n    },\n\n    valencia: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        ctx.fillStyle = \"#3A0339\";\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"exclusion\", 0.5],\n            [\"sepia\", 0.08],\n            [\"brightness\", 0.08],\n            [\"contrast\", 0.08],\n        ]);\n    },\n\n    xpro: (filter, cv) => {\n        const ctx = cv.getContext(\"2d\");\n        const gradient = ctx.createRadialGradient(\n            cv.width / 2,\n            cv.height / 2,\n            0,\n            cv.width / 2,\n            cv.height / 2,\n            Math.hypot(cv.width, cv.height) / 2\n        );\n        gradient.addColorStop(0.4, \"#E0E7E6\");\n        gradient.addColorStop(1, \"#2B2AA1\");\n        ctx.fillStyle = gradient;\n        ctx.fillRect(0, 0, cv.width, cv.height);\n        applyAll(filter, [\n            [\"blend\", cv, \"color-burn\", 0.7],\n            [\"sepia\", 0.3],\n        ]);\n    },\n\n    custom: (filter, cv, filterOptions) => {\n        const options = Object.assign(\n            {\n                blend: \"normal\",\n                filterColor: \"\",\n                blur: \"0\",\n                desaturateLuminance: \"0\",\n                saturation: \"0\",\n                contrast: \"0\",\n                brightness: \"0\",\n                sepia: \"0\",\n            },\n            JSON.parse(filterOptions || \"{}\")\n        );\n        const filters = [];\n        if (options.filterColor) {\n            const ctx = cv.getContext(\"2d\");\n            ctx.fillStyle = options.filterColor;\n            ctx.fillRect(0, 0, cv.width, cv.height);\n            filters.push([\"blend\", cv, options.blend, 1]);\n        }\n        delete options.blend;\n        delete options.filterColor;\n        filters.push(\n            ...Object.entries(options).map(([filter, amount]) => [filter, parseInt(amount) / 100])\n        );\n        applyAll(filter, filters);\n    },\n};\n\n/**\n * Applies data-attributes modifications to an img tag and returns a dataURL\n * containing the result. This function does not modify the original image.\n *\n * @param {HTMLImageElement} img the image to which modifications are applied\n * @param {Cropper} cropper the cropper instance\n * @returns {string} dataURL of the image with the applied modifications\n */\nexport async function applyModifications(img, cropper, dataOptions = {}) {\n    const data = Object.assign(\n        {\n            glFilter: \"\",\n            filter: \"#0000\",\n            quality: \"75\",\n            forceModification: false,\n        },\n        img.dataset,\n        dataOptions\n    );\n    let {\n        width,\n        height,\n        resizeWidth,\n        quality,\n        filter,\n        mimetype,\n        originalSrc,\n        glFilter,\n        filterOptions,\n        forceModification,\n        perspective,\n        svgAspectRatio,\n        imgAspectRatio,\n    } = data;\n    [width, height, resizeWidth] = [width, height, resizeWidth].map((s) => parseFloat(s));\n    quality = parseInt(quality);\n\n    // Skip modifications (required to add shapes on animated GIFs).\n    if (isGif(mimetype) && !forceModification) {\n        return await _loadImageDataURL(originalSrc);\n    }\n\n    // Crop\n    const container = document.createElement(\"div\");\n    const original = await loadImage(originalSrc);\n    // loadImage may have ended up loading a different src (see: LOAD_IMAGE_404)\n    originalSrc = original.getAttribute(\"src\");\n    container.appendChild(original);\n    let croppedImg = cropper.getCroppedCanvas(width, height);\n\n    // Aspect Ratio\n    if (imgAspectRatio) {\n        document.createElement(\"div\").appendChild(croppedImg);\n        imgAspectRatio = imgAspectRatio.split(\":\");\n        imgAspectRatio = parseFloat(imgAspectRatio[0]) / parseFloat(imgAspectRatio[1]);\n        const croppedCropper = await activateCropper(croppedImg, imgAspectRatio, { y: 0 });\n        croppedImg = croppedCropper.cropper(\"getCroppedCanvas\");\n        croppedCropper.destroy();\n    }\n\n    // Width\n    const result = document.createElement(\"canvas\");\n    result.width = resizeWidth || croppedImg.width;\n    result.height = perspective\n        ? result.width / svgAspectRatio\n        : (croppedImg.height * result.width) / croppedImg.width;\n    const ctx = result.getContext(\"2d\");\n    ctx.imageSmoothingQuality = \"high\";\n    ctx.mozImageSmoothingEnabled = true;\n    ctx.webkitImageSmoothingEnabled = true;\n    ctx.msImageSmoothingEnabled = true;\n    ctx.imageSmoothingEnabled = true;\n\n    // Perspective 3D\n    if (perspective) {\n        // x, y coordinates of the corners of the image as a percentage\n        // (relative to the width or height of the image) needed to apply\n        // the 3D effect.\n        const points = JSON.parse(perspective);\n        const divisions = 10;\n        const w = croppedImg.width,\n            h = croppedImg.height;\n\n        const project = getProjective(w, h, [\n            [(result.width / 100) * points[0][0], (result.height / 100) * points[0][1]], // Top-left [x, y]\n            [(result.width / 100) * points[1][0], (result.height / 100) * points[1][1]], // Top-right [x, y]\n            [(result.width / 100) * points[2][0], (result.height / 100) * points[2][1]], // bottom-right [x, y]\n            [(result.width / 100) * points[3][0], (result.height / 100) * points[3][1]], // bottom-left [x, y]\n        ]);\n\n        for (let i = 0; i < divisions; i++) {\n            for (let j = 0; j < divisions; j++) {\n                const [dx, dy] = [w / divisions, h / divisions];\n\n                const upper = {\n                    origin: [i * dx, j * dy],\n                    sides: [dx, dy],\n                    flange: 0.1,\n                    overlap: 0,\n                };\n                const lower = {\n                    origin: [i * dx + dx, j * dy + dy],\n                    sides: [-dx, -dy],\n                    flange: 0,\n                    overlap: 0.1,\n                };\n\n                for (let { origin, sides, flange, overlap } of [upper, lower]) {\n                    const [[a, c, e], [b, d, f]] = getAffineApproximation(project, [\n                        origin,\n                        [origin[0] + sides[0], origin[1]],\n                        [origin[0], origin[1] + sides[1]],\n                    ]);\n\n                    const ox = (i !== divisions ? overlap * sides[0] : 0) + flange * sides[0];\n                    const oy = (j !== divisions ? overlap * sides[1] : 0) + flange * sides[1];\n\n                    origin[0] += flange * sides[0];\n                    origin[1] += flange * sides[1];\n\n                    sides[0] -= flange * sides[0];\n                    sides[1] -= flange * sides[1];\n\n                    ctx.save();\n                    ctx.setTransform(a, b, c, d, e, f);\n\n                    ctx.beginPath();\n                    ctx.moveTo(origin[0] - ox, origin[1] - oy);\n                    ctx.lineTo(origin[0] + sides[0], origin[1] - oy);\n                    ctx.lineTo(origin[0] + sides[0], origin[1]);\n                    ctx.lineTo(origin[0], origin[1] + sides[1]);\n                    ctx.lineTo(origin[0] - ox, origin[1] + sides[1]);\n                    ctx.closePath();\n                    ctx.clip();\n                    ctx.drawImage(croppedImg, 0, 0);\n\n                    ctx.restore();\n                }\n            }\n        }\n    } else {\n        ctx.drawImage(\n            croppedImg,\n            0,\n            0,\n            croppedImg.width,\n            croppedImg.height,\n            0,\n            0,\n            result.width,\n            result.height\n        );\n    }\n\n    // GL filter\n    if (glFilter) {\n        const glf = new window.WebGLImageFilter();\n        const cv = document.createElement(\"canvas\");\n        cv.width = result.width;\n        cv.height = result.height;\n        applyAll = _applyAll.bind(null, result);\n        glFilters[glFilter](glf, cv, filterOptions);\n        const filtered = glf.apply(result);\n        ctx.drawImage(\n            filtered,\n            0,\n            0,\n            filtered.width,\n            filtered.height,\n            0,\n            0,\n            result.width,\n            result.height\n        );\n    }\n\n    // Color filter\n    ctx.fillStyle = filter || \"#0000\";\n    ctx.fillRect(0, 0, result.width, result.height);\n\n    // Quality\n    const dataURL = result.toDataURL(mimetype, quality / 100);\n    const newSize = getDataURLBinarySize(dataURL);\n    const originalSize = _getImageSizeFromCache(originalSrc);\n    const isChanged =\n        !!perspective ||\n        !!glFilter ||\n        original.width !== result.width ||\n        original.height !== result.height ||\n        original.width !== croppedImg.width ||\n        original.height !== croppedImg.height;\n    return isChanged || originalSize >= newSize ? dataURL : await _loadImageDataURL(originalSrc);\n}\n\n/**\n * Loads an src into an HTMLImageElement.\n *\n * @param {String} src URL of the image to load\n * @param {HTMLImageElement} [img] img element in which to load the image\n * @returns {Promise<HTMLImageElement>} Promise that resolves to the loaded img\n *     or a placeholder image if the src is not found.\n */\nexport function loadImage(src, img = new Image()) {\n    const handleImage = (source, resolve, reject) => {\n        img.addEventListener(\"load\", () => resolve(img), { once: true });\n        img.addEventListener(\"error\", reject, { once: true });\n        img.src = source;\n    };\n    // The server will return a placeholder image with the following src.\n    // grep: LOAD_IMAGE_404\n    const placeholderHref = \"/web/image/__odoo__unknown__src__/\";\n\n    return new Promise((resolve, reject) => {\n        fetch(src)\n            .then((response) => {\n                if (!response.ok) {\n                    src = placeholderHref;\n                }\n                handleImage(src, resolve, reject);\n            })\n            .catch((error) => {\n                src = placeholderHref;\n                handleImage(src, resolve, reject);\n            });\n    });\n}\n\n// Because cropperjs acquires images through XHRs on the image src and we don't\n// want to load big images over the network many times when adjusting quality\n// and filter, we create a local cache of the images using object URLs.\nconst imageCache = new Map();\n\n/**\n * Loads image object URL into cache if not already set and returns it.\n *\n * @param {String} src\n * @returns {Promise}\n */\nfunction _loadImageObjectURL(src) {\n    return _updateImageData(src);\n}\n\n/**\n * Gets image dataURL from cache in the same way as object URL.\n *\n * @param {String} src\n * @returns {Promise}\n */\nfunction _loadImageDataURL(src) {\n    return _updateImageData(src, \"dataURL\");\n}\n\n/**\n * @param {String} src used as a key on the image cache map.\n * @param {String} [key='objectURL'] specifies the image data to update/return.\n * @returns {Promise<String>} resolves with either dataURL/objectURL value.\n */\nasync function _updateImageData(src, key = \"objectURL\") {\n    const currentImageData = imageCache.get(src);\n    if (currentImageData && currentImageData[key]) {\n        return currentImageData[key];\n    }\n    let value = \"\";\n    const blob = await fetch(src).then((res) => res.blob());\n    if (key === \"dataURL\") {\n        value = await createDataURL(blob);\n    } else {\n        value = URL.createObjectURL(blob);\n    }\n    imageCache.set(src, Object.assign(currentImageData || {}, { [key]: value, size: blob.size }));\n    return value;\n}\n\n/**\n * Returns the size of a cached image.\n * Warning: this supposes that the image is already in the cache, i.e. that\n * _updateImageData was called before.\n *\n * @param {String} src used as a key on the image cache map.\n * @returns {Number} size of the image in bytes.\n */\nfunction _getImageSizeFromCache(src) {\n    return imageCache.get(src).size;\n}\n\n/**\n * Activates the cropper on a given image.\n *\n * @param {jQuery} $image the image on which to activate the cropper\n * @param {Number} aspectRatio the aspectRatio of the crop box\n * @param {DOMStringMap} dataset dataset containing the cropperDataFields\n */\nexport async function activateCropper(image, aspectRatio, dataset) {\n    const oldSrc = image.src;\n    const newSrc = await _loadImageObjectURL(image.getAttribute(\"src\"));\n    image.src = newSrc;\n    // eslint-disable-next-line no-undef\n    const cropper = new Cropper(image, {\n        viewMode: 2,\n        dragMode: \"move\",\n        autoCropArea: 1.0,\n        aspectRatio: aspectRatio,\n        data: Object.fromEntries(\n            Object.entries(pick(dataset, ...cropperDataFields)).map(([key, value]) => [\n                key,\n                parseFloat(value),\n            ])\n        ),\n        // Can't use 0 because it's falsy and cropperjs will then use its defaults (200x100)\n        minContainerWidth: 1,\n        minContainerHeight: 1,\n    });\n    if (oldSrc === newSrc && image.complete) {\n        return;\n    }\n    return cropper;\n}\n\n/**\n * Marks an <img> with its attachment data (originalId, originalSrc, mimetype)\n *\n * @param {HTMLImageElement} img the image whose attachment data should be found\n * @param {string} [attachmentSrc=''] specifies the URL of the corresponding\n * attachment if it can't be found in the 'src' attribute.\n */\nexport async function loadImageInfo(img, attachmentSrc = \"\") {\n    const src = attachmentSrc || img.getAttribute(\"src\");\n    // If there is a marked originalSrc, the data is already loaded.\n    // If the image does not have the \"mimetypeBeforeConversion\" attribute, it\n    // has to be added.\n    if ((img.dataset.originalSrc && img.dataset.mimetypeBeforeConversion) || !src) {\n        return;\n    }\n    // In order to be robust to absolute, relative and protocol relative URLs,\n    // the src of the img is first converted to an URL object. To do so, the URL\n    // of the document in which the img is located is used as a base to build\n    // the URL object if the src of the img is a relative or protocol relative\n    // URL. The original attachment linked to the img is then retrieved thanks\n    // to the path of the built URL object.\n    let docHref = img.ownerDocument.defaultView.location.href;\n    if (docHref.startsWith(\"about:\")) {\n        docHref = window.location.href;\n    }\n\n    const srcUrl = new URL(src, docHref);\n    const relativeSrc = srcUrl.pathname;\n\n    const { original } = await rpc(\"/html_editor/get_image_info\", { src: relativeSrc });\n    // If src was an absolute \"external\" URL, we consider unlikely that its\n    // relative part matches something from the DB and even if it does, nothing\n    // bad happens, besides using this random image as the original when using\n    // the options, instead of having no option. Note that we do not want to\n    // check if the image is local or not here as a previous bug converted some\n    // local (relative src) images to absolute URL... and that before users had\n    // setup their website domain. That means they can have an absolute URL that\n    // looks like \"https://mycompany.odoo.com/web/image/123\" that leads to a\n    // \"local\" image even if the domain name is now \"mycompany.be\".\n    //\n    // The \"redirect\" check is for when it is a redirect image attachment due to\n    // an external URL upload.\n    if (\n        original &&\n        original.image_src &&\n        !/\\/web\\/image\\/\\d+-redirect\\//.test(original.image_src)\n    ) {\n        if (!img.dataset.mimetype) {\n            // The mimetype has to be added only if it is not already present as\n            // we want to avoid to reset a mimetype set by the user.\n            img.dataset.mimetype = original.mimetype;\n        }\n        img.dataset.originalId = original.id;\n        img.dataset.originalSrc = original.image_src;\n        img.dataset.mimetypeBeforeConversion = original.mimetype;\n    }\n}\n\n/**\n * @param {Blob} blob\n * @returns {Promise}\n */\nexport function createDataURL(blob) {\n    return new Promise((resolve, reject) => {\n        const reader = new FileReader();\n        reader.addEventListener(\"load\", () => resolve(reader.result));\n        reader.addEventListener(\"abort\", reject);\n        reader.addEventListener(\"error\", reject);\n        reader.readAsDataURL(blob);\n    });\n}\n\n/**\n * @param {String} dataURL\n * @returns {Number} number of bytes represented with base64\n */\nexport function getDataURLBinarySize(dataURL) {\n    // Every 4 bytes of base64 represent 3 bytes.\n    return (dataURL.split(\",\")[1].length / 4) * 3;\n}\n", "import { removeClass, setTagName } from \"./dom\";\n\nexport function getListMode(pnode) {\n    if (![\"UL\", \"OL\"].includes(pnode.tagName)) {\n        return;\n    }\n    if (pnode.tagName === \"OL\") {\n        return \"OL\";\n    }\n    return pnode.classList.contains(\"o_checklist\") ? \"CL\" : \"UL\";\n}\n\n/**\n * Switches the list mode of the given list element.\n *\n * @param {HTMLOListElement|HTMLUListElement} list - The list element to switch the mode of.\n * @param {\"UL\"|\"OL\"|\"CL\"} newMode - The new mode to switch to.\n * @returns {HTMLOListElement|HTMLUListElement} The modified list element.\n */\nexport function switchListMode(list, newMode) {\n    if (getListMode(list) === newMode) {\n        return;\n    }\n    const newTag = newMode === \"CL\" ? \"UL\" : newMode;\n    const newList = setTagName(list, newTag);\n    // Clear list style (@todo @phoenix - why??)\n    newList.style.removeProperty(\"list-style\");\n    for (const li of newList.children) {\n        if (li.style.listStyle !== \"none\") {\n            li.style.listStyle = null;\n            if (!li.style.all) {\n                li.removeAttribute(\"style\");\n            }\n        }\n    }\n    removeClass(newList, \"o_checklist\");\n    if (newMode === \"CL\") {\n        newList.classList.add(\"o_checklist\");\n    }\n    return newList;\n}\n\n/**\n * Converts a list element and its nested elements to the given list mode.\n *\n * @see switchListMode\n * @param {HTMLUListElement|HTMLOListElement|HTMLLIElement} node - HTML element\n * representing a list or list item.\n * @param {string} newMode - Target list mode\n * @returns {HTMLUListElement|HTMLOListElement|HTMLLIElement} node - Modified\n * list element after conversion.\n */\nexport function convertList(node, newMode) {\n    if (![\"UL\", \"OL\", \"LI\"].includes(node.tagName)) {\n        return;\n    }\n    const listMode = getListMode(node);\n    if (listMode && newMode !== listMode) {\n        node = switchListMode(node, newMode);\n    }\n    for (const child of node.children) {\n        convertList(child, newMode);\n    }\n    return node;\n}\n", "/**\n * Transform a 2D point using a projective transformation matrix. Note that\n * this method is only well behaved for points that don't map to infinity!\n *\n * @param {number[][]} matrix - A projective transformation matrix\n * @param {number[]} point - A 2D point\n * @returns The transformed 2D point\n */\nexport function transform([[a, b, c], [d, e, f], [g, h, i]], [x, y]) {\n    let z = g * x + h * y + i;\n    return [(a * x + b * y + c) / z, (d * x + e * y + f) / z];\n}\n\n/**\n * Calculate the inverse of a 3x3 matrix assuming it is invertible.\n *\n * @param {number[][]} matrix - A 3x3 matrix\n * @returns The resulting 3x3 matrix\n */\nfunction invert([[a, b, c], [d, e, f], [g, h, i]]) {\n    const determinant = a * e * i - a * f * h - b * d * i + b * f * g + c * d * h - c * e * g;\n    return [\n        [(e * i - h * f) / determinant, (h * c - b * i) / determinant, (b * f - e * c) / determinant],\n        [(g * f - d * i) / determinant, (a * i - g * c) / determinant, (d * c - a * f) / determinant],\n        [(d * h - g * e) / determinant, (g * b - a * h) / determinant, (a * e - d * b) / determinant],\n    ];\n}\n\n/**\n * Multiply two 3x3 matrices.\n *\n * @param {number[][]} a - A 3x3 matrix\n * @param {number[][]} b - A 3x3 matrix\n * @returns The resulting 3x3 matrix\n */\nfunction multiply(a, b) {\n    const [[a0, a1, a2], [a3, a4, a5], [a6, a7, a8]] = a;\n    const [[b0, b1, b2], [b3, b4, b5], [b6, b7, b8]] = b;\n    return [\n        [a0 * b0 + a1 * b3 + a2 * b6, a0 * b1 + a1 * b4 + a2 * b7, a0 * b2 + a1 * b5 + a2 * b8],\n        [a3 * b0 + a4 * b3 + a5 * b6, a3 * b1 + a4 * b4 + a5 * b7, a3 * b2 + a4 * b5 + a5 * b8],\n        [a6 * b0 + a7 * b3 + a8 * b6, a6 * b1 + a7 * b4 + a8 * b7, a6 * b2 + a7 * b5 + a8 * b8],\n    ];\n}\n\n/**\n * Find a projective transformation mapping a rectangular area at origin (0,0)\n * with a given width and height to a certain quadrilateral.\n *\n * @param {number} width - The width of the rectangular area\n * @param {number} height - The height of the rectangular area\n * @param {number[][]} quadrilateral - The vertices of the quadrilateral\n * @returns A projective transformation matrix\n */\nexport function getProjective(width, height, [[x0, y0], [x1, y1], [x2, y2], [x3, y3]]) {\n    // Calculate a set of homogeneous coordinates a, b, c of the first\n    // point using the other three points as basis vectors in the\n    // underlying vector space.\n    const denominator = x3 * (y1 - y2) + x1 * (y2 - y3) + x2 * (y3 - y1);\n    const a = (x0 * (y2 - y3) + x2 * (y3 - y0) + x3 * (y0 - y2)) / denominator;\n    const b = (x0 * (y3 - y1) + x3 * (y1 - y0) + x1 * (y0 - y3)) / denominator;\n    const c = (x0 * (y1 - y2) + x1 * (y2 - y0) + x2 * (y0 - y1)) / denominator;\n\n    // The reverse transformation maps the homogeneous coordinates of\n    // the last three corners of the original image onto the basis vectors\n    // while mapping the first corner onto (1, 1, 1). The forward\n    // transformation maps those basis vectors in addition to (1, 1, 1)\n    // onto homogeneous coordinates of the corresponding corners of the\n    // projective image. Combining these together yields the projective\n    // transformation we are looking for.\n    const reverse = invert([[width, -width, 0], [0, -height, height], [1, -1, 1]]);\n    const forward = [[a * x1, b * x2, c * x3], [a * y1, b * y2, c * y3], [a, b, c]];\n\n    return multiply(forward, reverse);\n}\n\n/**\n * Find an affine transformation matrix that exactly maps the vertices of a\n * triangle to their corresponding images of a projective transformation. The\n * resulting transformation will be an approximation of the projective\n * transformation for the area inside the triangle.\n *\n * @param {number[][]} projective - A projective transformation matrix\n * @param {number[][]} triangle - The vertices of a triangle\n * @returns - An affine transformation matrix\n */\nexport function getAffineApproximation(projective, [[x0, y0], [x1, y1], [x2, y2]]) {\n    const a = transform(projective, [x0, y0]);\n    const b = transform(projective, [x1, y1]);\n    const c = transform(projective, [x2, y2]);\n\n    return multiply(\n        [[a[0], b[0], c[0]], [a[1], b[1], c[1]], [1, 1, 1]],\n        invert([[x0, x1, x2], [y0, y1, y2], [1, 1, 1]]),\n    );\n}\n", "// Position and sizes\n//------------------------------------------------------------------------------\n\nexport const DIRECTIONS = {\n    LEFT: false,\n    RIGHT: true,\n};\n\n/**\n * @param {Node} node\n * @returns {[HTMLElement, number]}\n */\nexport function leftPos(node) {\n    return [node.parentElement, childNodeIndex(node)];\n}\n/**\n * @param {Node} node\n * @returns {[HTMLElement, number]}\n */\nexport function rightPos(node) {\n    return [node.parentElement, childNodeIndex(node) + 1];\n}\n/**\n * @param {Node} node\n * @returns {[HTMLElement, number, HTMLElement, number]}\n */\nexport function boundariesOut(node) {\n    const index = childNodeIndex(node);\n    return [node.parentElement, index, node.parentElement, index + 1];\n}\n/**\n * @param {Node} node\n * @returns {[HTMLElement, number, HTMLElement, number]}\n */\nexport function boundariesIn(node) {\n    return [node, 0, node, nodeSize(node)];\n}\n/**\n * @param {Node} node\n * @returns {[Node, number]}\n */\nexport function startPos(node) {\n    return [node, 0];\n}\n/**\n * @param {Node} node\n * @returns {[Node, number]}\n */\nexport function endPos(node) {\n    return [node, nodeSize(node)];\n}\n/**\n * Returns the given node's position relative to its parent (= its index in the\n * child nodes of its parent).\n *\n * @param {Node} node\n * @returns {number}\n */\nexport function childNodeIndex(node) {\n    let i = 0;\n    while (node.previousSibling) {\n        i++;\n        node = node.previousSibling;\n    }\n    return i;\n}\n/**\n * Returns the size of the node = the number of characters for text nodes and\n * the number of child nodes for element nodes.\n *\n * @param {Node} node\n * @returns {number}\n */\nexport function nodeSize(node) {\n    const isTextNode = node.nodeType === Node.TEXT_NODE;\n    return isTextNode ? node.length : node.childNodes.length;\n}\n", "/* eslint-disable */\n\nconst tldWhitelist = [\n    'com', 'net', 'org', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an',\n    'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd',\n    'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bl', 'bm', 'bn', 'bo', 'br', 'bq',\n    'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch',\n    'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cs', 'cu', 'cv', 'cw', 'cx',\n    'cy', 'cz', 'dd', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg',\n    'eh', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga',\n    'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', 'gq',\n    'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu',\n    'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm',\n    'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky',\n    'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly',\n    'ma', 'mc', 'md', 'me', 'mf', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo',\n    'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na',\n    'nc', 'ne', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om',\n    'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt',\n    'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 'sc', 'sd',\n    'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', 'ss',\n    'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj',\n    'tk', 'tl', 'tm', 'tn', 'to', 'tp', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua',\n    'ug', 'uk', 'um', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn',\n    'vu', 'wf', 'ws', 'ye', 'yt', 'yu', 'za', 'zm', 'zr', 'zw', 'co\\\\.uk'];\n\nconst urlRegexBase = `|(?:www.))[-a-zA-Z0-9@:%._\\\\+~#=]{2,256}\\\\.[a-zA-Z][a-zA-Z0-9]{1,62}|(?:[-a-zA-Z0-9@:%._\\\\+~#=]{2,256}\\\\.(?:${tldWhitelist.join('|')})\\\\b))(?:(?:[/?#])[^\\\\s]*[^!.,})\\\\]'\"\\\\s]|(?:[^!(){}.,[\\\\]'\"\\\\s]+))?`;\nconst httpCapturedRegex= `(https?:\\\\/\\\\/)`;\n\nexport const URL_REGEX = new RegExp(`((?:(?:${httpCapturedRegex}${urlRegexBase})`, 'i');\n", "export const resourceSequenceSymbol = Symbol(\"resourceSequence\");\n\nexport function withSequence(sequenceNumber, object) {\n    return {\n        [resourceSequenceSymbol]: sequenceNumber,\n        object,\n    };\n}\n", "import { isBlock } from \"./blocks\";\nimport { isPhrasingContent } from \"../utils/dom_info\";\n\n// @todo @phoenix: consider using the wrapInlinesInBlocks utils instead.\n\nexport function initElementForEdition(element, options = {}) {\n    const document = element.ownerDocument;\n    // Detect if the editable base element contain orphan inline nodes. If\n    // so we transform the base element HTML to put those orphans inside\n    // `<p>` containers.\n    const orphanInlineChildNodes = [...element.childNodes].find(\n        (n) => !isBlock(n) && (n.nodeType === Node.ELEMENT_NODE || n.textContent.trim() !== \"\")\n    );\n    if (orphanInlineChildNodes && !options.allowInlineAtRoot) {\n        const childNodes = [...element.childNodes];\n        const blockMap = new WeakMap();\n        for (const node of childNodes) {\n            blockMap.set(node, isBlock(node));\n        }\n        const newChildren = [];\n        let currentBlock = document.createElement(\"DIV\");\n        let hasOnlyPhrasingContent = true;\n        currentBlock.style.marginBottom = \"0\";\n        for (let i = 0; i < childNodes.length; i++) {\n            const node = childNodes[i];\n            const nodeIsBlock = blockMap.get(node);\n            const nodeIsBR = node.nodeName === \"BR\";\n            // Append to the P unless child is block or an unneeded BR.\n            if (!(nodeIsBlock || (nodeIsBR && currentBlock.hasChildNodes()))) {\n                currentBlock.append(node);\n                if (!isPhrasingContent(node)) {\n                    hasOnlyPhrasingContent = false;\n                }\n            }\n            // Break paragraphs on blocks and BR.\n            if (nodeIsBlock || nodeIsBR || childNodes.length === i + 1) {\n                if (hasOnlyPhrasingContent) {\n                    const block = document.createElement(\"P\");\n                    block.style.marginBottom = \"0\";\n                    block.replaceChildren(...currentBlock.childNodes);\n                    currentBlock = block;\n                }\n                // Ensure we don't add an empty P or a P containing only\n                // formating spaces that should not be visible.\n                if (currentBlock.hasChildNodes() && currentBlock.innerHTML.trim() !== \"\") {\n                    newChildren.push(currentBlock);\n                }\n                currentBlock = document.createElement(\"DIV\");\n                currentBlock.style.marginBottom = \"0\";\n                hasOnlyPhrasingContent = true;\n                // Append block children directly to the template.\n                if (nodeIsBlock) {\n                    newChildren.push(node);\n                }\n            }\n        }\n        element.replaceChildren(...newChildren);\n    }\n}\n", "import { closestBlock, isBlock } from \"./blocks\";\nimport {\n    isContentEditable,\n    isNotEditableNode,\n    isSelfClosingElement,\n    nextLeaf,\n    previousLeaf,\n} from \"./dom_info\";\nimport { isFakeLineBreak } from \"./dom_state\";\nimport { closestElement, createDOMPathGenerator } from \"./dom_traversal\";\nimport {\n    DIRECTIONS,\n    childNodeIndex,\n    endPos,\n    leftPos,\n    nodeSize,\n    rightPos,\n    startPos,\n} from \"./position\";\n\n/**\n * @typedef { import(\"./selection_plugin\").EditorSelection } EditorSelection\n */\n\n/**\n * From selection position, checks if it is left-to-right or right-to-left.\n *\n * @param {Node} anchorNode\n * @param {number} anchorOffset\n * @param {Node} focusNode\n * @param {number} focusOffset\n * @returns {boolean} the direction of the current range if the selection not is collapsed | false\n */\nexport function getCursorDirection(anchorNode, anchorOffset, focusNode, focusOffset) {\n    if (anchorNode === focusNode) {\n        if (anchorOffset === focusOffset) {\n            return false;\n        }\n        return anchorOffset < focusOffset ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n    }\n    return anchorNode.compareDocumentPosition(focusNode) & Node.DOCUMENT_POSITION_FOLLOWING\n        ? DIRECTIONS.RIGHT\n        : DIRECTIONS.LEFT;\n}\n\n/**\n * @param {EditorSelection} selection\n * @param {string} selector\n */\nexport function findInSelection(selection, selector) {\n    const selectorInStartAncestors = closestElement(selection.startContainer, selector);\n    if (selectorInStartAncestors) {\n        return selectorInStartAncestors;\n    } else {\n        const commonElementAncestor = closestElement(selection.commonAncestorContainer);\n        return (\n            commonElementAncestor &&\n            [...commonElementAncestor.querySelectorAll(selector)].find((node) =>\n                selection.intersectsNode(node)\n            )\n        );\n    }\n}\n\nconst leftLeafOnlyInScopeNotBlockEditablePath = createDOMPathGenerator(DIRECTIONS.LEFT, {\n    leafOnly: true,\n    inScope: true,\n    stopTraverseFunction: (node) => isNotEditableNode(node) || isBlock(node),\n    stopFunction: (node) => isNotEditableNode(node) || isBlock(node),\n});\n\nconst rightLeafOnlyInScopeNotBlockEditablePath = createDOMPathGenerator(DIRECTIONS.RIGHT, {\n    leafOnly: true,\n    inScope: true,\n    stopTraverseFunction: (node) => isNotEditableNode(node) || isBlock(node),\n    stopFunction: (node) => isNotEditableNode(node) || isBlock(node),\n});\n\nexport function normalizeSelfClosingElement(node, offset) {\n    if (isSelfClosingElement(node)) {\n        // Cannot put cursor inside those elements, put it after instead.\n        [node, offset] = rightPos(node);\n    }\n    return [node, offset];\n}\n\nexport function normalizeNotEditableNode(node, offset, position = \"right\") {\n    const editable = closestElement(node, \".odoo-editor-editable\");\n    let closest = closestElement(node);\n    while (closest && closest !== editable && !closest.isContentEditable) {\n        [node, offset] = position === \"right\" ? rightPos(node) : leftPos(node);\n        closest = node;\n    }\n    return [node, offset];\n}\n\nexport function normalizeCursorPosition(node, offset, position = \"right\") {\n    [node, offset] = normalizeSelfClosingElement(node, offset);\n    [node, offset] = normalizeNotEditableNode(node, offset, position);\n    // todo @phoenix: we should maybe remove it\n    // // Be permissive about the received offset.\n    // offset = Math.min(Math.max(offset, 0), nodeSize(node));\n    return [node, offset];\n}\n\nexport function normalizeFakeBR(node, offset) {\n    const prevNode = node.nodeType === Node.ELEMENT_NODE && node.childNodes[offset - 1];\n    if (prevNode && prevNode.nodeName === \"BR\" && isFakeLineBreak(prevNode)) {\n        // If trying to put the cursor on the right of a fake line break, put\n        // it before instead.\n        offset--;\n    }\n    return [node, offset];\n}\n\n/**\n * From a given position, returns the normalized version.\n *\n * E.g. <b>abc</b>[]def -> <b>abc[]</b>def\n *\n * @param {Node} node\n * @param {number} offset\n * @returns { [Node, number] }\n */\nexport function normalizeDeepCursorPosition(node, offset) {\n    // Put the cursor in deepest inline node around the given position if\n    // possible.\n    let el;\n    let elOffset;\n    if (node.nodeType === Node.ELEMENT_NODE) {\n        el = node;\n        elOffset = offset;\n    } else if (node.nodeType === Node.TEXT_NODE) {\n        if (offset === 0) {\n            el = node.parentNode;\n            elOffset = childNodeIndex(node);\n        } else if (offset === node.length) {\n            el = node.parentNode;\n            elOffset = childNodeIndex(node) + 1;\n        }\n    }\n    if (el) {\n        const leftInlineNode = leftLeafOnlyInScopeNotBlockEditablePath(el, elOffset).next().value;\n        let leftVisibleEmpty = false;\n        if (leftInlineNode) {\n            leftVisibleEmpty =\n                isSelfClosingElement(leftInlineNode) || !isContentEditable(leftInlineNode);\n            [node, offset] = leftVisibleEmpty ? rightPos(leftInlineNode) : endPos(leftInlineNode);\n        }\n        if (!leftInlineNode || leftVisibleEmpty) {\n            const rightInlineNode = rightLeafOnlyInScopeNotBlockEditablePath(el, elOffset).next()\n                .value;\n            if (rightInlineNode) {\n                const closest = closestElement(rightInlineNode);\n                const rightVisibleEmpty =\n                    isSelfClosingElement(rightInlineNode) || !closest || !closest.isContentEditable;\n                if (!(leftVisibleEmpty && rightVisibleEmpty)) {\n                    [node, offset] = rightVisibleEmpty\n                        ? leftPos(rightInlineNode)\n                        : startPos(rightInlineNode);\n                }\n            }\n        }\n    }\n    return [node, offset];\n}\n\nfunction updateCursorBeforeMove(destParent, destIndex, node, cursor) {\n    if (cursor.node === destParent && cursor.offset >= destIndex) {\n        // Update cursor at destination\n        cursor.offset += 1;\n    } else if (cursor.node === node.parentNode) {\n        const childIndex = childNodeIndex(node);\n        // Update cursor at origin\n        if (cursor.offset === childIndex) {\n            // Keep pointing to the moved node\n            [cursor.node, cursor.offset] = [destParent, destIndex];\n        } else if (cursor.offset > childIndex) {\n            cursor.offset -= 1;\n        }\n    }\n}\n\nfunction updateCursorBeforeRemove(node, cursor) {\n    if (node.contains(cursor.node)) {\n        [cursor.node, cursor.offset] = [node.parentNode, childNodeIndex(node)];\n    } else if (cursor.node === node.parentNode && cursor.offset > childNodeIndex(node)) {\n        cursor.offset -= 1;\n    }\n}\n\nfunction updateCursorBeforeUnwrap(node, cursor) {\n    if (cursor.node === node) {\n        [cursor.node, cursor.offset] = [node.parentNode, cursor.offset + childNodeIndex(node)];\n    } else if (cursor.node === node.parentNode && cursor.offset > childNodeIndex(node)) {\n        cursor.offset += nodeSize(node) - 1;\n    }\n}\n\nfunction updateCursorBeforeMergeIntoPreviousSibling(node, cursor) {\n    if (cursor.node === node) {\n        cursor.node = node.previousSibling;\n        cursor.offset += node.previousSibling.childNodes.length;\n    } else if (cursor.node === node.parentNode) {\n        const childIndex = childNodeIndex(node);\n        if (cursor.offset === childIndex) {\n            cursor.node = node.previousSibling;\n            cursor.offset = node.previousSibling.childNodes.length;\n        } else if (cursor.offset > childIndex) {\n            cursor.offset--;\n        }\n    }\n}\n\n/** @typedef {import(\"@html_editor/core/selection_plugin\").Cursor} Cursor */\n\nexport const callbacksForCursorUpdate = {\n    /** @type {(node: Node) => (cursor: Cursor) => void} */\n    remove: (node) => (cursor) => updateCursorBeforeRemove(node, cursor),\n    /** @type {(ref: HTMLElement, node: Node) => (cursor: Cursor) => void} */\n    before: (ref, node) => (cursor) =>\n        updateCursorBeforeMove(ref.parentNode, childNodeIndex(ref), node, cursor),\n    /** @type {(ref: HTMLElement, node: Node) => (cursor: Cursor) => void} */\n    after: (ref, node) => (cursor) =>\n        updateCursorBeforeMove(ref.parentNode, childNodeIndex(ref) + 1, node, cursor),\n    /** @type {(ref: HTMLElement, node: Node) => (cursor: Cursor) => void} */\n    append: (to, node) => (cursor) =>\n        updateCursorBeforeMove(to, to.childNodes.length, node, cursor),\n    /** @type {(ref: HTMLElement, node: Node) => (cursor: Cursor) => void} */\n    prepend: (to, node) => (cursor) => updateCursorBeforeMove(to, 0, node, cursor),\n    /** @type {(node: HTMLElement) => (cursor: Cursor) => void} */\n    unwrap: (node) => (cursor) => updateCursorBeforeUnwrap(node, cursor),\n    /** @type {(node: HTMLElement) => (cursor: Cursor) => void} */\n    merge: (node) => (cursor) => updateCursorBeforeMergeIntoPreviousSibling(node, cursor),\n};\n\n/**\n * @param {Selection} selection\n * @param {\"previous\"|\"next\"} side\n * @param {HTMLElement} editable\n * @returns {string | undefined}\n */\nexport function getAdjacentCharacter(selection, side, editable) {\n    let { focusNode, focusOffset } = selection;\n    const originalBlock = closestBlock(focusNode);\n    let adjacentCharacter;\n    while (!adjacentCharacter && focusNode) {\n        if (side === \"previous\") {\n            // @todo: this might be wrong in the first time, as focus node might not be a leaf.\n            adjacentCharacter = focusOffset > 0 && focusNode.textContent[focusOffset - 1];\n        } else {\n            adjacentCharacter = focusNode.textContent[focusOffset];\n        }\n        if (!adjacentCharacter) {\n            if (side === \"previous\") {\n                focusNode = previousLeaf(focusNode, editable);\n                focusOffset = focusNode && nodeSize(focusNode);\n            } else {\n                focusNode = nextLeaf(focusNode, editable);\n                focusOffset = 0;\n            }\n            const characterIndex = side === \"previous\" ? focusOffset - 1 : focusOffset;\n            adjacentCharacter = focusNode && focusNode.textContent[characterIndex];\n        }\n    }\n    if (!focusNode || !isContentEditable(focusNode) || closestBlock(focusNode) !== originalBlock) {\n        return undefined;\n    }\n    return adjacentCharacter;\n}\n", "import { closestElement } from \"./dom_traversal\";\n\n/**\n * Get the index of the given table row/cell.\n *\n * @private\n * @param {HTMLTableRowElement|HTMLTableCellElement} trOrTd\n * @returns {number}\n */\nexport function getRowIndex(trOrTd) {\n    const tr = closestElement(trOrTd, \"tr\");\n    return tr.rowIndex;\n}\n\n/**\n * Get the index of the given table cell.\n *\n * @private\n * @param {HTMLTableCellElement} td\n * @returns {number}\n */\nexport function getColumnIndex(td) {\n    return td.cellIndex;\n}\n", "export const excalidrawWebsiteDomainList = [\n    \"excalidraw.com\",\n    \"link.excalidraw.com\",\n    \"app.excalidraw.com\",\n];\n\n/**\n * Checks if the given URL contains the specified hostname and returns a reconstructed URL if it does.\n *\n * @param {string} url - The URL to be checked\n * @param {Array} hostname - The hostname to be included in the modified URL\n * @return {string|boolean} The modified URL with the specified hostname included, or false if the URL does not meet the conditions\n */\nexport function checkURL(url, hostnameList) {\n    if (url) {\n        let potentialURL;\n        try {\n            potentialURL = new URL(url);\n        } catch {\n            return false;\n        }\n        if (hostnameList.includes(potentialURL.hostname)) {\n            return `https://${potentialURL.hostname}${potentialURL.pathname}`;\n        }\n    }\n    return false;\n}\n\n/**\n * @param {string} url\n */\nexport function isImageUrl(url) {\n    const urlFileExtention = url.split(\".\").pop();\n    return [\"jpg\", \"jpeg\", \"png\", \"gif\", \"svg\", \"webp\"].includes(urlFileExtention.toLowerCase());\n}\n\n/**\n * @param {string} platform\n * @param {string} videoId\n * @param {Object} params\n * @throws {Error} if the given video config is not recognized\n * @returns {URL}\n */\nexport function getVideoUrl(platform, videoId, params) {\n    let url;\n    switch (platform) {\n        case \"youtube\":\n            url = new URL(`https://www.youtube.com/embed/${videoId}`);\n            break;\n        case \"vimeo\":\n            url = new URL(`https://player.vimeo.com/video/${videoId}`);\n            break;\n        case \"dailymotion\":\n            url = new URL(`https://www.dailymotion.com/embed/video/${videoId}`);\n            break;\n        case \"instagram\":\n            url = new URL(`https://www.instagram.com/p/${videoId}/embed`);\n            break;\n        case \"youku\":\n            url = new URL(`https://player.youku.com/embed/${videoId}`);\n            break;\n        default:\n            throw new Error(`Unsupported platform: ${platform}`);\n    }\n    url.search = new URLSearchParams(params);\n    return url;\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ImageSelector } from \"@html_editor/main/media/media_dialog/image_selector\";\n\nimport { UnsplashError } from \"../unsplash_error/unsplash_error\";\nimport { useState } from \"@odoo/owl\";\n\npatch(ImageSelector.prototype, {\n    setup() {\n        super.setup();\n        this.unsplash = useService(\"unsplash\");\n        this.keepLastUnsplash = new KeepLast();\n        this.unsplashState = useState({\n            unsplashRecords: [],\n            isFetchingUnsplash: false,\n            isMaxed: false,\n            unsplashError: null,\n            useUnsplash: true,\n        });\n\n        this.NUMBER_OF_RECORDS_TO_DISPLAY = 30;\n\n        this.errorMessages = {\n            key_not_found: {\n                title: _t(\"Setup Unsplash to access royalty free photos.\"),\n                subtitle: \"\",\n            },\n            401: {\n                title: _t(\"Unauthorized Key\"),\n                subtitle: _t(\"Please check your Unsplash access key and application ID.\"),\n            },\n            403: {\n                title: _t(\"Search is temporarily unavailable\"),\n                subtitle: _t(\n                    \"The max number of searches is exceeded. Please retry in an hour or extend to a better account.\"\n                ),\n            },\n        };\n    },\n\n    get canLoadMore() {\n        if (this.state.searchService === \"all\") {\n            return (\n                super.canLoadMore ||\n                (this.state.needle &&\n                    !this.unsplashState.isMaxed &&\n                    !this.unsplashState.unsplashError)\n            );\n        } else if (this.state.searchService === \"unsplash\") {\n            return (\n                this.state.needle &&\n                !this.unsplashState.isMaxed &&\n                !this.unsplashState.unsplashError\n            );\n        }\n        return super.canLoadMore;\n    },\n\n    get hasContent() {\n        if (this.state.searchService === \"all\") {\n            return super.hasContent || !!this.unsplashState.unsplashRecords.length;\n        } else if (this.state.searchService === \"unsplash\") {\n            return !!this.unsplashState.unsplashRecords.length;\n        }\n        return super.hasContent;\n    },\n\n    get errorTitle() {\n        if (this.errorMessages[this.unsplashState.unsplashError]) {\n            return this.errorMessages[this.unsplashState.unsplashError].title;\n        }\n        return _t(\"Something went wrong\");\n    },\n\n    get errorSubtitle() {\n        if (this.errorMessages[this.unsplashState.unsplashError]) {\n            return this.errorMessages[this.unsplashState.unsplashError].subtitle;\n        }\n        return _t(\"Please check your internet connection or contact administrator.\");\n    },\n\n    get selectedRecordIds() {\n        return this.props.selectedMedia[this.props.id]\n            .filter((media) => media.mediaType === \"unsplashRecord\")\n            .map(({ id }) => id);\n    },\n\n    get isFetching() {\n        return super.isFetching || this.unsplashState.isFetchingUnsplash;\n    },\n\n    get combinedRecords() {\n        /**\n         * Creates an array with alternating elements from two arrays.\n         *\n         * @param {Array} a\n         * @param {Array} b\n         * @returns {Array} alternating elements from a and b, starting with\n         *     an element of a\n         */\n        function alternate(a, b) {\n            return [a.map((v, i) => (i < b.length ? [v, b[i]] : v)), b.slice(a.length)].flat(2);\n        }\n        return alternate(this.unsplashState.unsplashRecords, this.state.libraryMedia);\n    },\n\n    get allAttachments() {\n        return [...super.allAttachments, ...this.unsplashState.unsplashRecords];\n    },\n\n    async fetchUnsplashRecords(offset) {\n        if (!this.state.needle) {\n            return { records: [], isMaxed: false };\n        }\n        this.unsplashState.isFetchingUnsplash = true;\n        try {\n            const { isMaxed, images } = await this.unsplash.getImages(\n                this.state.needle,\n                offset,\n                this.NUMBER_OF_RECORDS_TO_DISPLAY,\n                this.props.orientation\n            );\n            this.unsplashState.isFetchingUnsplash = false;\n            this.unsplashState.unsplashError = false;\n            // Ignore duplicates.\n            const existingIds = this.unsplashState.unsplashRecords.map((existing) => existing.id);\n            const newImages = images.filter((record) => !existingIds.includes(record.id));\n            const records = newImages.map((record) => {\n                const url = new URL(record.urls.regular);\n                // In small windows, row height could get quite a bit larger than the min, so we keep some leeway.\n                url.searchParams.set(\"h\", 2 * this.MIN_ROW_HEIGHT);\n                url.searchParams.delete(\"w\");\n                return Object.assign({}, record, {\n                    url: url.toString(),\n                    mediaType: \"unsplashRecord\",\n                });\n            });\n            return { isMaxed, records };\n        } catch (e) {\n            this.unsplashState.isFetchingUnsplash = false;\n            if (e === \"no_access\") {\n                this.unsplashState.useUnsplash = false;\n            } else {\n                this.unsplashState.unsplashError = e;\n            }\n            return { records: [], isMaxed: true };\n        }\n    },\n\n    async loadMore(...args) {\n        await super.loadMore(...args);\n        return this.keepLastUnsplash\n            .add(this.fetchUnsplashRecords(this.unsplashState.unsplashRecords.length))\n            .then(({ records, isMaxed }) => {\n                // This is never reached if another search or loadMore occurred.\n                this.unsplashState.unsplashRecords.push(...records);\n                this.unsplashState.isMaxed = isMaxed;\n            });\n    },\n\n    async search(...args) {\n        await super.search(...args);\n        await this.searchUnsplash();\n    },\n\n    async searchUnsplash() {\n        if (!this.state.needle) {\n            this.unsplashState.unsplashError = false;\n            this.unsplashState.unsplashRecords = [];\n            this.unsplashState.isMaxed = false;\n        }\n        return this.keepLastUnsplash\n            .add(this.fetchUnsplashRecords(0))\n            .then(({ records, isMaxed }) => {\n                // This is never reached if a new search occurred.\n                this.unsplashState.unsplashRecords = records;\n                this.unsplashState.isMaxed = isMaxed;\n            });\n    },\n\n    async onClickRecord(media) {\n        this.props.selectMedia({ ...media, mediaType: \"unsplashRecord\", query: this.state.needle });\n        if (!this.props.multiSelect) {\n            await this.props.save();\n        }\n    },\n\n    async submitCredentials(key, appId) {\n        this.unsplashState.unsplashError = null;\n        await rpc(\"/web_unsplash/save_unsplash\", { key, appId });\n        await this.searchUnsplash();\n    },\n});\n\nImageSelector.components = {\n    ...ImageSelector.components,\n    UnsplashError,\n};\n", "import { MediaDialog, TABS } from \"@html_editor/main/media/media_dialog/media_dialog\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\n\npatch(MediaDialog.prototype, {\n    setup() {\n        super.setup();\n        this.unsplashService = useService(\"unsplash\");\n    },\n\n    async save() {\n        const selectedImages = this.selectedMedia[TABS.IMAGES.id];\n        if (selectedImages) {\n            const unsplashRecords = selectedImages.filter(\n                (media) => media.mediaType === \"unsplashRecord\"\n            );\n            if (unsplashRecords.length) {\n                await this.unsplashService.uploadUnsplashRecords(\n                    unsplashRecords,\n                    { resModel: this.props.resModel, resId: this.props.resId },\n                    (attachments) => {\n                        this.selectedMedia[TABS.IMAGES.id] = this.selectedMedia[\n                            TABS.IMAGES.id\n                        ].filter((media) => media.mediaType !== \"unsplashRecord\");\n                        this.selectedMedia[TABS.IMAGES.id] = this.selectedMedia[\n                            TABS.IMAGES.id\n                        ].concat(\n                            attachments.map((attachment) => ({\n                                ...attachment,\n                                mediaType: \"attachment\",\n                            }))\n                        );\n                    }\n                );\n            }\n        }\n        return super.save(...arguments);\n    },\n});\n", "import { Component, useState } from \"@odoo/owl\";\n\nexport class UnsplashCredentials extends Component {\n    static template = \"web_unsplash.UnsplashCredentials\";\n    static props = {\n        submitCredentials: Function,\n        hasCredentialsError: Boolean,\n    };\n    setup() {\n        this.state = useState({\n            key: \"\",\n            appId: \"\",\n            hasKeyError: this.props.hasCredentialsError,\n            hasAppIdError: this.props.hasCredentialsError,\n        });\n    }\n\n    submitCredentials() {\n        if (this.state.key === \"\") {\n            this.state.hasKeyError = true;\n        } else if (this.state.appId === \"\") {\n            this.state.hasAppIdError = true;\n        } else {\n            this.props.submitCredentials(this.state.key, this.state.appId);\n        }\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { UnsplashCredentials } from \"../unsplash_credentials/unsplash_credentials\";\n\nexport class UnsplashError extends Component {\n    static template = \"web_unsplash.UnsplashError\";\n    static components = {\n        UnsplashCredentials,\n    };\n    static props = {\n        title: String,\n        subtitle: String,\n        showCredentials: Boolean,\n        submitCredentials: { type: Function, optional: true },\n        hasCredentialsError: { type: Boolean, optional: true },\n    };\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { AUTOCLOSE_DELAY } from \"@html_editor/main/media/media_dialog/upload_progress_toast/upload_service\";\n\nexport const unsplashService = {\n    dependencies: [\"upload\"],\n    async start(env, { upload }) {\n        const _cache = {};\n        return {\n            async uploadUnsplashRecords(records, { resModel, resId }, onUploaded) {\n                upload.incrementId();\n                const file = upload.addFile({\n                    id: upload.fileId,\n                    name:\n                        records.length > 1\n                            ? _t(\"Uploading %(count)s '%(query)s' images.\", {\n                                  count: records.length,\n                                  query: records[0].query,\n                              })\n                            : _t(\"Uploading '%s' image.\", records[0].query),\n                });\n\n                try {\n                    const urls = {};\n                    for (const record of records) {\n                        const _1920Url = new URL(record.urls.regular);\n                        _1920Url.searchParams.set(\"w\", \"1920\");\n                        urls[record.id] = {\n                            url: _1920Url.href,\n                            download_url: record.links.download_location,\n                            description: record.alt_description,\n                        };\n                    }\n\n                    const xhr = new XMLHttpRequest();\n                    xhr.upload.addEventListener(\"progress\", (ev) => {\n                        const rpcComplete = (ev.loaded / ev.total) * 100;\n                        file.progress = rpcComplete;\n                    });\n                    xhr.upload.addEventListener(\"load\", function () {\n                        // Don't show yet success as backend code only starts now\n                        file.progress = 100;\n                    });\n                    const attachments = await rpc(\n                        \"/web_unsplash/attachment/add\",\n                        {\n                            res_id: resId,\n                            res_model: resModel,\n                            unsplashurls: urls,\n                            query: records[0].query,\n                        },\n                        { xhr }\n                    );\n\n                    if (attachments.error) {\n                        file.hasError = true;\n                        file.errorMessage = attachments.error;\n                    } else {\n                        file.uploaded = true;\n                        await onUploaded(attachments);\n                    }\n                    setTimeout(() => upload.deleteFile(file.id), AUTOCLOSE_DELAY);\n                } catch (error) {\n                    file.hasError = true;\n                    setTimeout(() => upload.deleteFile(file.id), AUTOCLOSE_DELAY);\n                    throw error;\n                }\n            },\n\n            async getImages(query, offset = 0, pageSize = 30, orientation) {\n                const from = offset;\n                const to = offset + pageSize;\n                // Use orientation in the cache key to not show images in cache\n                // when using the same query word but changing the orientation\n                let cachedData = orientation ? _cache[query + orientation] : _cache[query];\n\n                if (\n                    cachedData &&\n                    (cachedData.images.length >= to ||\n                        (cachedData.totalImages !== 0 && cachedData.totalImages < to))\n                ) {\n                    return {\n                        images: cachedData.images.slice(from, to),\n                        isMaxed: to > cachedData.totalImages,\n                    };\n                }\n                cachedData = await this._fetchImages(query, orientation);\n                return {\n                    images: cachedData.images.slice(from, to),\n                    isMaxed: to > cachedData.totalImages,\n                };\n            },\n            /**\n             * Fetches images from unsplash and stores it in cache\n             */\n            async _fetchImages(query, orientation) {\n                const key = orientation ? query + orientation : query;\n                if (!_cache[key]) {\n                    _cache[key] = {\n                        images: [],\n                        maxPages: 0,\n                        totalImages: 0,\n                        pageCached: 0,\n                    };\n                }\n                const cachedData = _cache[key];\n                const payload = {\n                    query: query,\n                    page: cachedData.pageCached + 1,\n                    per_page: 30, // max size from unsplash API\n                };\n                if (orientation) {\n                    payload.orientation = orientation;\n                }\n                const result = await rpc(\"/web_unsplash/fetch_images\", payload);\n                if (result.error) {\n                    return Promise.reject(result.error);\n                }\n                cachedData.pageCached++;\n                cachedData.images.push(...result.results);\n                cachedData.maxPages = result.total_pages;\n                cachedData.totalImages = result.total;\n                return cachedData;\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"unsplash\", unsplashService);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Attachment, FileSelector, IMAGE_MIMETYPES } from './file_selector';\n\nexport class DocumentAttachment extends Attachment {\n    static template = \"web_editor.DocumentAttachment\";\n}\n\nexport class DocumentSelector extends FileSelector {\n    static mediaSpecificClasses = [\"o_image\"];\n    static mediaSpecificStyles = [];\n    static mediaExtraClasses = [];\n    static tagNames = [\"A\"];\n    static attachmentsListTemplate = \"web_editor.DocumentsListTemplate\";\n    static components = {\n        ...FileSelector.components,\n        DocumentAttachment,\n    };\n\n    setup() {\n        super.setup();\n\n        this.uploadText = _t(\"Upload a document\");\n        this.urlPlaceholder = \"https://www.odoo.com/mydocument\";\n        this.addText = _t(\"Add URL\");\n        this.searchPlaceholder = _t(\"Search a document\");\n        this.allLoadedText = _t(\"All documents have been loaded\");\n    }\n\n    get attachmentsDomain() {\n        const domain = super.attachmentsDomain;\n        domain.push(['mimetype', 'not in', IMAGE_MIMETYPES]);\n        // The assets should not be part of the documents.\n        // All assets begin with '/web/assets/', see _get_asset_template_url().\n        domain.unshift('&', '|', ['url', '=', null], '!', ['url', '=like', '/web/assets/%']);\n        return domain;\n    }\n\n    async onClickDocument(document) {\n        this.selectAttachment(document);\n        await this.props.save();\n    }\n\n    async fetchAttachments(...args) {\n        const attachments = await super.fetchAttachments(...args);\n\n        if (this.selectInitialMedia()) {\n            for (const attachment of attachments) {\n                if (`/web/content/${attachment.id}` === this.props.media.getAttribute('href').replace(/[?].*/, '')) {\n                    this.selectAttachment(attachment);\n                }\n            }\n        }\n        return attachments;\n    }\n\n    /**\n     * Utility method used by the MediaDialog component.\n     */\n    static async createElements(selectedMedia, { orm }) {\n        return Promise.all(selectedMedia.map(async attachment => {\n            const linkEl = document.createElement('a');\n            let href = `/web/content/${encodeURIComponent(attachment.id)}?unique=${encodeURIComponent(attachment.checksum)}&download=true`;\n            if (!attachment.public) {\n                let accessToken = attachment.access_token;\n                if (!accessToken) {\n                    [accessToken] = await orm.call(\n                        'ir.attachment',\n                        'generate_access_token',\n                        [attachment.id],\n                    );\n                }\n                href += `&access_token=${encodeURIComponent(accessToken)}`;\n            }\n            linkEl.href = href;\n            linkEl.title = attachment.name;\n            linkEl.dataset.mimetype = attachment.mimetype;\n            return linkEl;\n        }));\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from '@web/core/utils/hooks';\nimport { ConfirmationDialog } from '@web/core/confirmation_dialog/confirmation_dialog';\nimport { Dialog } from '@web/core/dialog/dialog';\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { SearchMedia } from './search_media';\n\nimport { Component, xml, useState, useRef, onWillStart, useEffect } from \"@odoo/owl\";\n\nexport const IMAGE_MIMETYPES = ['image/jpg', 'image/jpeg', 'image/jpe', 'image/png', 'image/svg+xml', 'image/gif', 'image/webp'];\nexport const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.jpe', '.png', '.svg', '.gif', '.webp'];\n\nclass RemoveButton extends Component {\n    static template = xml`<i class=\"fa fa-trash o_existing_attachment_remove position-absolute top-0 end-0 p-2 bg-white-25 cursor-pointer opacity-0 opacity-100-hover z-1 transition-base\" t-att-title=\"removeTitle\" role=\"img\" t-att-aria-label=\"removeTitle\" t-on-click=\"this.remove\"/>`;\n    static props = [\"model?\", \"remove\"];\n    setup() {\n        this.removeTitle = _t(\"This file is attached to the current record.\");\n        if (this.props.model === 'ir.ui.view') {\n            this.removeTitle = _t(\"This file is a public view attachment.\");\n        }\n    }\n\n    remove(ev) {\n        ev.stopPropagation();\n        this.props.remove();\n    }\n}\n\nexport class AttachmentError extends Component {\n    static components = { Dialog };\n    static template = xml`\n        <Dialog title=\"title\">\n            <div class=\"form-text\">\n                <p>The image could not be deleted because it is used in the\n                    following pages or views:</p>\n                <ul t-foreach=\"props.views\"  t-as=\"view\" t-key=\"view.id\">\n                    <li>\n                        <a t-att-href=\"'/odoo/ir.ui.view/' + window.encodeURIComponent(view.id)\">\n                            <t t-esc=\"view.name\"/>\n                        </a>\n                    </li>\n                </ul>\n            </div>\n            <t t-set-slot=\"footer\">\n                <button class=\"btn btn-primary\" t-on-click=\"() => this.props.close()\">\n                    Ok\n                </button>\n            </t>\n        </Dialog>`;\n    static props = [\"views\", \"close\"];\n    setup() {\n        this.title = _t(\"Alert\");\n    }\n}\n\nexport class Attachment extends Component {\n    static template = \"\";\n    static components = {\n        RemoveButton,\n    };\n    static props = [\"*\"];\n    setup() {\n        this.dialogs = useService('dialog');\n    }\n\n    remove() {\n        this.dialogs.add(ConfirmationDialog, {\n            body: _t(\"Are you sure you want to delete this file?\"),\n            confirm: async () => {\n                const prevented = await rpc('/web_editor/attachment/remove', {\n                    ids: [this.props.id],\n                });\n                if (!Object.keys(prevented).length) {\n                    this.props.onRemoved(this.props.id);\n                } else {\n                    this.dialogs.add(AttachmentError, {\n                        views: prevented[this.props.id],\n                    });\n                }\n            },\n        });\n    }\n}\n\nexport class FileSelectorControlPanel extends Component {\n    static template = \"web_editor.FileSelectorControlPanel\";\n    static components = {\n        SearchMedia,\n    };\n    static props = {\n        uploadUrl: Function,\n        validateUrl: Function,\n        uploadFiles: Function,\n        changeSearchService: Function,\n        changeShowOptimized: Function,\n        search: Function,\n        accept: {type: String, optional: true},\n        addText: {type: String, optional: true},\n        multiSelect: {type: true, optional: true},\n        needle: {type: String, optional: true},\n        searchPlaceholder: {type: String, optional: true},\n        searchService: {type: String, optional: true},\n        showOptimized: {type: Boolean, optional: true},\n        showOptimizedOption: {type: String, optional: true},\n        uploadText: {type: String, optional: true},\n        urlPlaceholder: {type: String, optional: true},\n        urlWarningTitle: {type: String, optional: true},\n        useMediaLibrary: {type: Boolean, optional: true},\n        useUnsplash: {type: Boolean, optional: true},\n    };\n    setup() {\n        this.state = useState({\n            showUrlInput: false,\n            urlInput: '',\n            isValidUrl: false,\n            isValidFileFormat: false,\n            isValidatingUrl: false,\n        });\n        this.debouncedValidateUrl = useDebounced(this.props.validateUrl, 500);\n\n        this.fileInput = useRef('file-input');\n    }\n\n    get showSearchServiceSelect() {\n        return this.props.searchService && this.props.needle;\n    }\n\n    get enableUrlUploadClick() {\n        return !this.state.showUrlInput || (this.state.urlInput && this.state.isValidUrl && this.state.isValidFileFormat);\n    }\n\n    async onUrlUploadClick() {\n        if (!this.state.showUrlInput) {\n            this.state.showUrlInput = true;\n        } else {\n            await this.props.uploadUrl(this.state.urlInput);\n            this.state.urlInput = '';\n        }\n    }\n\n    async onUrlInput(ev) {\n        this.state.isValidatingUrl = true;\n        const { isValidUrl, isValidFileFormat } = await this.debouncedValidateUrl(ev.target.value);\n        this.state.isValidFileFormat = isValidFileFormat;\n        this.state.isValidUrl = isValidUrl;\n        this.state.isValidatingUrl = false;\n    }\n\n    onClickUpload() {\n        this.fileInput.el.click();\n    }\n\n    async onChangeFileInput() {\n        const inputFiles = this.fileInput.el.files;\n        if (!inputFiles.length) {\n            return;\n        }\n        await this.props.uploadFiles(inputFiles);\n        this.fileInput.el.value = '';\n    }\n}\n\nexport class FileSelector extends Component {\n    static template = \"web_editor.FileSelector\";\n    static components = {\n        FileSelectorControlPanel,\n    };\n    static props = [\"*\"];\n\n    setup() {\n        this.notificationService = useService(\"notification\");\n        this.orm = useService('orm');\n        this.uploadService = useService('upload');\n        this.keepLast = new KeepLast();\n\n        this.loadMoreButtonRef = useRef('load-more-button');\n        this.existingAttachmentsRef = useRef(\"existing-attachments\");\n\n        this.state = useState({\n            attachments: [],\n            canScrollAttachments: false,\n            canLoadMoreAttachments: false,\n            isFetchingAttachments: false,\n            needle: '',\n        });\n\n        this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY = 30;\n\n        onWillStart(async () => {\n            this.state.attachments = await this.fetchAttachments(this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY, 0);\n        });\n\n        this.debouncedOnScroll = useDebounced(this.updateScroll, 15);\n        this.debouncedScrollUpdate = useDebounced(this.updateScroll, 500);\n\n        useEffect(\n            (modalEl) => {\n                if (modalEl) {\n                    modalEl.addEventListener(\"scroll\", this.debouncedOnScroll);\n                    return () => {\n                        modalEl.removeEventListener(\"scroll\", this.debouncedOnScroll);\n                    };\n                }\n            },\n            () => [this.props.modalRef.el?.querySelector(\"main.modal-body\")]\n        );\n\n        useEffect(\n            () => {\n                // Updating the scroll button each time the attachments change.\n                // Hiding the \"Load more\" button to prevent it from flickering.\n                this.loadMoreButtonRef.el.classList.add(\"o_hide_loading\");\n                this.state.canScrollAttachments = false;\n                this.debouncedScrollUpdate();\n            },\n            () => [this.allAttachments.length]);\n    }\n\n    get canLoadMore() {\n        return this.state.canLoadMoreAttachments;\n    }\n\n    get hasContent() {\n        return this.state.attachments.length;\n    }\n\n    get isFetching() {\n        return this.state.isFetchingAttachments;\n    }\n\n    get selectedAttachmentIds() {\n        return this.props.selectedMedia[this.props.id].filter(media => media.mediaType === 'attachment').map(({ id }) => id);\n    }\n\n    get attachmentsDomain() {\n        const domain = [\n            '&',\n            ['res_model', '=', this.props.resModel],\n            ['res_id', '=', this.props.resId || 0]\n        ];\n        domain.unshift('|', ['public', '=', true]);\n        domain.push(['name', 'ilike', this.state.needle]);\n        return domain;\n    }\n\n    get allAttachments() {\n        return this.state.attachments;\n    }\n\n    validateUrl(url) {\n        const path = url.split('?')[0];\n        const isValidUrl = /^.+\\..+$/.test(path); // TODO improve\n        const isValidFileFormat = true;\n        return { isValidUrl, isValidFileFormat, path };\n    }\n\n    async fetchAttachments(limit, offset) {\n        this.state.isFetchingAttachments = true;\n        let attachments = [];\n        try {\n            attachments = await this.orm.call(\n                'ir.attachment',\n                'search_read',\n                [],\n                {\n                    domain: this.attachmentsDomain,\n                    fields: ['name', 'mimetype', 'description', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src', 'image_width', 'image_height', 'original_id'],\n                    order: 'id desc',\n                    // Try to fetch first record of next page just to know whether there is a next page.\n                    limit,\n                    offset,\n                }\n            );\n            attachments.forEach(attachment => attachment.mediaType = 'attachment');\n        } catch (e) {\n            // Reading attachments as a portal user is not permitted and will raise\n            // an access error so we catch the error silently and don't return any\n            // attachment so he can still use the wizard and upload an attachment\n            if (e.exceptionName !== 'odoo.exceptions.AccessError') {\n                throw e;\n            }\n        }\n        this.state.canLoadMoreAttachments = attachments.length >= this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY;\n        this.state.isFetchingAttachments = false;\n        return attachments;\n    }\n\n    async handleLoadMore() {\n        await this.loadMore();\n    }\n\n    async loadMore() {\n        return this.keepLast.add(this.fetchAttachments(this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY, this.state.attachments.length)).then((newAttachments) => {\n            // This is never reached if another search or loadMore occurred.\n            this.state.attachments.push(...newAttachments);\n        });\n    }\n\n    async handleSearch(needle) {\n        await this.search(needle);\n    }\n\n    async search(needle) {\n        // Prepare in case loadMore results are obtained instead.\n        this.state.attachments = [];\n        // Fetch attachments relies on the state's needle.\n        this.state.needle = needle;\n        return this.keepLast.add(this.fetchAttachments(this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY, 0)).then((attachments) => {\n            // This is never reached if a new search occurred.\n            this.state.attachments = attachments;\n        });\n    }\n\n    async uploadFiles(files) {\n        await this.uploadService.uploadFiles(files, { resModel: this.props.resModel, resId: this.props.resId }, attachment => this.onUploaded(attachment));\n    }\n\n    async uploadUrl(url) {\n        await this.uploadService.uploadUrl(url, {\n            resModel: this.props.resModel,\n            resId: this.props.resId,\n        }, attachment => this.onUploaded(attachment));\n    }\n\n    async onUploaded(attachment) {\n        this.state.attachments = [attachment, ...this.state.attachments.filter(attach => attach.id !== attachment.id)];\n        this.selectAttachment(attachment);\n        if (!this.props.multiSelect) {\n            await this.props.save();\n        }\n        if (this.props.onAttachmentChange) {\n            this.props.onAttachmentChange(attachment);\n        }\n    }\n\n    onRemoved(attachmentId) {\n        this.state.attachments = this.state.attachments.filter(attachment => attachment.id !== attachmentId);\n    }\n\n    selectAttachment(attachment) {\n        this.props.selectMedia({ ...attachment, mediaType: 'attachment' });\n    }\n\n    selectInitialMedia() {\n        return this.props.media\n            && this.constructor.tagNames.includes(this.props.media.tagName)\n            && !this.selectedAttachmentIds.length;\n    }\n\n    /**\n     * Updates the scroll button, depending on whether the \"Load more\" button is\n     * fully visible or not.\n     */\n    updateScroll() {\n        const loadMoreTop = this.loadMoreButtonRef.el.getBoundingClientRect().top;\n        const modalEl = this.props.modalRef.el.querySelector(\"main.modal-body\");\n        const modalBottom = modalEl.getBoundingClientRect().bottom;\n        this.state.canScrollAttachments = loadMoreTop >= modalBottom;\n        this.loadMoreButtonRef.el.classList.remove(\"o_hide_loading\");\n    }\n\n    /**\n     * Checks if the attachment is (partially) hidden.\n     *\n     * @param {Element} attachmentEl the attachment \"container\"\n     * @returns {Boolean} true if the attachment is hidden, false otherwise.\n     */\n    isAttachmentHidden(attachmentEl) {\n        const attachmentBottom = Math.round(attachmentEl.getBoundingClientRect().bottom);\n        const modalEl = this.props.modalRef.el.querySelector(\"main.modal-body\");\n        const modalBottom = modalEl.getBoundingClientRect().bottom;\n        return attachmentBottom > modalBottom;\n    }\n\n    /**\n     * Scrolls two attachments rows at a time. If there are not enough rows,\n     * scrolls to the \"Load more\" button.\n     */\n    handleScrollAttachments() {\n        let scrollToEl = this.loadMoreButtonRef.el;\n        const attachmentEls = [...this.existingAttachmentsRef.el.querySelectorAll(\".o_existing_attachment_cell\")];\n        const firstHiddenAttachmentEl = attachmentEls.find(el => this.isAttachmentHidden(el));\n        if (firstHiddenAttachmentEl) {\n            const attachmentBottom = firstHiddenAttachmentEl.getBoundingClientRect().bottom;\n            const attachmentIndex = attachmentEls.indexOf(firstHiddenAttachmentEl);\n            const firstNextRowAttachmentEl = attachmentEls.slice(attachmentIndex).find(el => {\n                return el.getBoundingClientRect().bottom > attachmentBottom;\n            })\n            scrollToEl = firstNextRowAttachmentEl || scrollToEl;\n        }\n        scrollToEl.scrollIntoView({ block: \"end\", inline: \"nearest\", behavior: \"smooth\" });\n    }\n}\n", "import fonts from '@web_editor/js/wysiwyg/fonts';\nimport { SearchMedia } from './search_media';\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class IconSelector extends Component {\n    static mediaSpecificClasses = [\"fa\"];\n    static mediaSpecificStyles = [\"color\", \"background-color\", \"border-width\", \"border-color\", \"border-style\"];\n    static mediaExtraClasses = [\n        \"rounded-circle\",\n        \"rounded\",\n        \"img-thumbnail\",\n        \"shadow\",\n        \"border\",\n        /^text-\\S+$/,\n        /^bg-\\S+$/,\n        /^fa-\\S+$/,\n    ];\n    static tagNames = [\"SPAN\", \"I\"];\n    static template = \"web_editor.IconSelector\";\n    static components = {\n        SearchMedia,\n    };\n    static props = [\"*\"];\n\n    setup() {\n        this.state = useState({\n            fonts: this.props.fonts,\n            needle: '',\n        });\n    }\n\n    get selectedMediaIds() {\n        return this.props.selectedMedia[this.props.id].map(({ id }) => id);\n    }\n\n    search(needle) {\n        this.state.needle = needle;\n        if (!this.state.needle) {\n            this.state.fonts = this.props.fonts;\n        } else {\n            this.state.fonts = this.props.fonts.map(font => {\n                const icons = font.icons.filter(icon => icon.alias.indexOf(this.state.needle) >= 0);\n                return {...font, icons};\n            });\n        }\n    }\n\n    async onClickIcon(font, icon) {\n        this.props.selectMedia({\n            ...icon,\n            fontBase: font.base,\n            // To check if the icon has changed, we only need to compare\n            // an alias of the icon with the class from the old media (some\n            // icons can have multiple classes e.g. \"fa-gears\" ~ \"fa-cogs\")\n            initialIconChanged: this.props.media\n                && !icon.names.some(name => this.props.media.classList.contains(name)),\n        });\n        await this.props.save();\n    }\n\n    /**\n     * Utility methods, used by the MediaDialog component.\n     */\n    static createElements(selectedMedia) {\n        return selectedMedia.map(icon => {\n            const iconEl = document.createElement('span');\n            iconEl.classList.add(icon.fontBase, icon.names[0]);\n            return iconEl;\n        });\n    }\n    static initFonts() {\n        fonts.computeFonts();\n        const allFonts = fonts.fontIcons.map(({cssData, base}) => {\n            const uniqueIcons = Array.from(new Map(cssData.map(icon => {\n                const alias = icon.names.join(',');\n                const id = `${base}_${alias}`;\n                return [id, { ...icon, alias, id }];\n            })).values());\n            return { base, icons: uniqueIcons };\n        });\n        return allFonts;\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport weUtils from '@web_editor/js/common/utils';\nimport { Attachment, FileSelector, IMAGE_MIMETYPES, IMAGE_EXTENSIONS } from './file_selector';\nimport { KeepLast } from \"@web/core/utils/concurrency\";\n\nimport { useRef, useState, useEffect } from \"@odoo/owl\";\n\nexport class AutoResizeImage extends Attachment {\n    static template = \"web_editor.AutoResizeImage\";\n    setup() {\n        super.setup();\n\n        this.image = useRef('auto-resize-image');\n        this.container = useRef('auto-resize-image-container');\n\n        this.state = useState({\n            loaded: false,\n        });\n\n        useEffect(() => {\n            this.image.el.addEventListener('load', () => this.onImageLoaded());\n            return this.image.el.removeEventListener('load', () => this.onImageLoaded());\n        }, () => []);\n    }\n\n    async onImageLoaded() {\n        if (!this.image.el) {\n            // Do not fail if already removed.\n            return;\n        }\n        if (this.props.onLoaded) {\n            await this.props.onLoaded(this.image.el);\n            if (!this.image.el) {\n                // If replaced by colored version, aspect ratio will be\n                // computed on it instead.\n                return;\n            }\n        }\n        const aspectRatio = this.image.el.offsetWidth / this.image.el.offsetHeight;\n        const width = aspectRatio * this.props.minRowHeight;\n        this.container.el.style.flexGrow = width;\n        this.container.el.style.flexBasis = `${width}px`;\n        this.state.loaded = true;\n    }\n}\nconst newLocal = \"img-fluid\";\nexport class ImageSelector extends FileSelector {\n    static mediaSpecificClasses = [\"img\", newLocal, \"o_we_custom_image\"];\n    static mediaSpecificStyles = [];\n    static mediaExtraClasses = [\n        \"rounded-circle\",\n        \"rounded\",\n        \"img-thumbnail\",\n        \"shadow\",\n        \"w-25\",\n        \"w-50\",\n        \"w-75\",\n        \"w-100\",\n    ];\n    static tagNames = [\"IMG\"];\n    static attachmentsListTemplate = \"web_editor.ImagesListTemplate\";\n    static components = {\n        ...FileSelector.components,\n        AutoResizeImage,\n    };\n\n    setup() {\n        super.setup();\n\n        this.keepLastLibraryMedia = new KeepLast();\n\n        this.state.libraryMedia = [];\n        this.state.libraryResults = null;\n        this.state.isFetchingLibrary = false;\n        this.state.searchService = 'all';\n        this.state.showOptimized = false;\n        this.NUMBER_OF_MEDIA_TO_DISPLAY = 10;\n\n        this.uploadText = _t(\"Upload an image\");\n        this.urlPlaceholder = \"https://www.odoo.com/logo.png\";\n        this.addText = _t(\"Add URL\");\n        this.searchPlaceholder = _t(\"Search an image\");\n        this.urlWarningTitle = _t(\"Uploaded image's format is not supported. Try with: \" + IMAGE_EXTENSIONS.join(', '));\n        this.allLoadedText = _t(\"All images have been loaded\");\n        this.showOptimizedOption = this.env.debug;\n        this.MIN_ROW_HEIGHT = 128;\n\n        this.fileMimetypes = IMAGE_MIMETYPES.join(',');\n    }\n\n    get canLoadMore() {\n        // The user can load more library media only when the filter is set.\n        if (this.state.searchService === 'media-library') {\n            return this.state.libraryResults && this.state.libraryMedia.length < this.state.libraryResults;\n        }\n        return super.canLoadMore;\n    }\n\n    get hasContent() {\n        if (this.state.searchService === 'all') {\n            return super.hasContent || !!this.state.libraryMedia.length;\n        } else if (this.state.searchService === 'media-library') {\n            return !!this.state.libraryMedia.length;\n        }\n        return super.hasContent;\n    }\n\n    get isFetching() {\n        return super.isFetching || this.state.isFetchingLibrary;\n    }\n\n    get selectedMediaIds() {\n        return this.props.selectedMedia[this.props.id].filter(media => media.mediaType === 'libraryMedia').map(({ id }) => id);\n    }\n\n    get allAttachments() {\n        return [...super.allAttachments, ...this.state.libraryMedia];\n    }\n\n    get attachmentsDomain() {\n        const domain = super.attachmentsDomain;\n        domain.push(['mimetype', 'in', IMAGE_MIMETYPES]);\n        if (!this.props.useMediaLibrary) {\n            domain.push(\"|\", [\"url\", \"=\", false],\n                \"!\", \"|\", [\"url\", \"=ilike\", \"/html_editor/shape/%\"], [\"url\", \"=ilike\", \"/web_editor/shape/%\"],\n            );\n        }\n        domain.push('!', ['name', '=like', '%.crop']);\n        domain.push('|', ['type', '=', 'binary'], '!', ['url', '=like', '/%/static/%']);\n\n        // Optimized images (meaning they are related to an `original_id`) can\n        // only be shown in debug mode as the toggler to make those images\n        // appear is hidden when not in debug mode.\n        // There is thus no point to fetch those optimized images outside debug\n        // mode. Worst, it leads to bugs: it might fetch only optimized images\n        // when clicking on \"load more\" which will look like it's bugged as no\n        // images will appear on screen (they all will be hidden).\n        if (!this.env.debug) {\n            const subDomain = [false];\n\n            // Particular exception: if the edited image is an optimized\n            // image, we need to fetch it too so it's displayed as the\n            // selected image when opening the media dialog.\n            // We might get a few more optimized image than necessary if the\n            // original image has multiple optimized images but it's not a\n            // big deal.\n            const originalId = this.props.media && this.props.media.dataset.originalId;\n            if (originalId) {\n                subDomain.push(originalId);\n            }\n\n            domain.push(['original_id', 'in', subDomain]);\n        }\n\n        return domain;\n    }\n\n    async uploadFiles(files) {\n        await this.uploadService.uploadFiles(files, { resModel: this.props.resModel, resId: this.props.resId, isImage: true }, (attachment) => this.onUploaded(attachment));\n    }\n\n    async uploadUrl(url) {\n        await fetch(url).then(async result => {\n            const blob = await result.blob();\n            blob.id = new Date().getTime();\n            blob.name = new URL(url, window.location.href).pathname.split(\"/\").findLast(s => s);\n            await this.uploadFiles([blob]);\n        }).catch(async () => {\n            await new Promise(resolve => {\n                // If it works from an image, use URL.\n                const imageEl = document.createElement(\"img\");\n                imageEl.onerror = () => {\n                    // This message is about the blob fetch failure.\n                    // It is only displayed if the fallback did not work.\n                    this.notificationService.add(_t(\"An error occurred while fetching the entered URL.\"), {\n                        title: _t(\"Error\"),\n                        sticky: true,\n                    });\n                    resolve();\n                };\n                imageEl.onload = () => {\n                    super.uploadUrl(url).then(resolve);\n                };\n                imageEl.src = url;\n            });\n        });\n    }\n\n    async validateUrl(...args) {\n        const { isValidUrl, path } = super.validateUrl(...args);\n        const isValidFileFormat = isValidUrl && await new Promise(resolve => {\n            const img = new Image();\n            img.src = path;\n            img.onload = () => resolve(true);\n            img.onerror = () => resolve(false);\n        });\n        return { isValidUrl, isValidFileFormat };\n    }\n\n    isInitialMedia(attachment) {\n        if (this.props.media.dataset.originalSrc) {\n            return this.props.media.dataset.originalSrc === attachment.image_src;\n        }\n        return this.props.media.getAttribute('src') === attachment.image_src;\n    }\n\n    async fetchAttachments(limit, offset) {\n        const attachments = await super.fetchAttachments(limit, offset);\n        // Color-substitution for dynamic SVG attachment\n        const primaryColors = {};\n        for (let color = 1; color <= 5; color++) {\n            primaryColors[color] = weUtils.getCSSVariableValue('o-color-' + color);\n        }\n        return attachments.map(attachment => {\n            if (attachment.image_src.startsWith('/')) {\n                const newURL = new URL(attachment.image_src, window.location.origin);\n                // Set the main colors of dynamic SVGs to o-color-1~5\n                if (attachment.image_src.startsWith('/html_editor/shape/') ||\n                    attachment.image_src.startsWith('/web_editor/shape/')\n                ) {\n                    newURL.searchParams.forEach((value, key) => {\n                        const match = key.match(/^c([1-5])$/);\n                        if (match) {\n                            newURL.searchParams.set(key, primaryColors[match[1]]);\n                        }\n                    });\n                } else {\n                    // Set height so that db images load faster\n                    newURL.searchParams.set('height', 2 * this.MIN_ROW_HEIGHT);\n                }\n                attachment.thumbnail_src = newURL.pathname + newURL.search;\n            }\n            if (this.selectInitialMedia() && this.isInitialMedia(attachment)) {\n                this.selectAttachment(attachment);\n            }\n            return attachment;\n        });\n    }\n\n    async fetchLibraryMedia(offset) {\n        if (!this.state.needle) {\n            return { media: [], results: null };\n        }\n\n        this.state.isFetchingLibrary = true;\n        try {\n            const response = await rpc(\n                '/web_editor/media_library_search',\n                {\n                    'query': this.state.needle,\n                    'offset': offset,\n                },\n                {\n                    silent: true,\n                }\n            );\n            this.state.isFetchingLibrary = false;\n            const media = (response.media || []).slice(0, this.NUMBER_OF_MEDIA_TO_DISPLAY);\n            media.forEach(record => record.mediaType = 'libraryMedia');\n            return { media, results: response.results };\n        } catch {\n            // Either API endpoint doesn't exist or is misconfigured.\n            console.error(`Couldn't reach API endpoint.`);\n            this.state.isFetchingLibrary = false;\n            return { media: [], results: null };\n        }\n    }\n\n    async loadMore(...args) {\n        await super.loadMore(...args);\n        if (!this.props.useMediaLibrary\n            // The user can load more library media only when the filter is set.\n            || this.state.searchService !== 'media-library'\n        ) {\n            return;\n        }\n        return this.keepLastLibraryMedia.add(this.fetchLibraryMedia(this.state.libraryMedia.length)).then(({ media }) => {\n            // This is never reached if another search or loadMore occurred.\n            this.state.libraryMedia.push(...media);\n        });\n    }\n\n    async search(...args) {\n        await super.search(...args);\n        if (!this.props.useMediaLibrary) {\n            return;\n        }\n        if (!this.state.needle) {\n            this.state.searchService = 'all';\n        }\n        this.state.libraryMedia = [];\n        this.state.libraryResults = 0;\n        return this.keepLastLibraryMedia.add(this.fetchLibraryMedia(0)).then(({ media, results }) => {\n            // This is never reached if a new search occurred.\n            this.state.libraryMedia = media;\n            this.state.libraryResults = results;\n        });\n    }\n\n    async onClickAttachment(attachment) {\n        this.selectAttachment(attachment);\n        if (!this.props.multiSelect) {\n            await this.props.save();\n        }\n    }\n\n    async onClickMedia(media) {\n        this.props.selectMedia({ ...media, mediaType: 'libraryMedia' });\n        if (!this.props.multiSelect) {\n            await this.props.save();\n        }\n    }\n\n    /**\n     * Utility method used by the MediaDialog component.\n     */\n    static async createElements(selectedMedia, { orm }) {\n        // Create all media-library attachments.\n        const toSave = Object.fromEntries(selectedMedia.filter(media => media.mediaType === 'libraryMedia').map(media => [\n            media.id, {\n                query: media.query || '',\n                is_dynamic_svg: !!media.isDynamicSVG,\n                dynamic_colors: media.dynamicColors,\n            }\n        ]));\n        let savedMedia = [];\n        if (Object.keys(toSave).length !== 0) {\n            savedMedia = await rpc('/web_editor/save_library_media', { media: toSave });\n        }\n        const selected = selectedMedia.filter(media => media.mediaType === 'attachment').concat(savedMedia).map(attachment => {\n            // Color-customize dynamic SVGs with the theme colors\n            if (attachment.image_src && (\n                attachment.image_src.startsWith('/html_editor/shape/') ||\n                attachment.image_src.startsWith('/web_editor/shape/')\n            )) {\n                const colorCustomizedURL = new URL(attachment.image_src, window.location.origin);\n                colorCustomizedURL.searchParams.forEach((value, key) => {\n                    const match = key.match(/^c([1-5])$/);\n                    if (match) {\n                        colorCustomizedURL.searchParams.set(key, weUtils.getCSSVariableValue(`o-color-${match[1]}`));\n                    }\n                });\n                attachment.image_src = colorCustomizedURL.pathname + colorCustomizedURL.search;\n            }\n            return attachment;\n        });\n        return Promise.all(selected.map(async (attachment) => {\n            const imageEl = document.createElement('img');\n            let src = attachment.image_src;\n            if (!attachment.public && !attachment.url) {\n                let accessToken = attachment.access_token;\n                if (!accessToken) {\n                    [accessToken] = await orm.call(\n                        'ir.attachment',\n                        'generate_access_token',\n                        [attachment.id],\n                    );\n                }\n                src += `?access_token=${encodeURIComponent(accessToken)}`;\n            }\n            imageEl.src = src;\n            imageEl.alt = attachment.description || '';\n            return imageEl;\n        }));\n    }\n\n    async onImageLoaded(imgEl, attachment) {\n        this.debouncedScrollUpdate();\n        if (attachment.mediaType === 'libraryMedia' && !imgEl.src.startsWith('blob')) {\n            // This call applies the theme's color palette to the\n            // loaded illustration. Upon replacement of the image,\n            // `onImageLoad` is called again, but the replacement image\n            // has an URL that starts with 'blob'. The condition above\n            // uses this to avoid an infinite loop.\n            await this.onLibraryImageLoaded(imgEl, attachment);\n        }\n    }\n\n    /**\n     * This converts the colors of an svg coming from the media library to\n     * the palette's ones, and make them dynamic.\n     *\n     * @param {HTMLElement} imgEl\n     * @param {Object} media\n     * @returns\n     */\n    async onLibraryImageLoaded(imgEl, media) {\n        const mediaUrl = imgEl.src;\n        try {\n            const response = await fetch(mediaUrl);\n            if (response.headers.get('content-type') === 'image/svg+xml') {\n                let svg = await response.text();\n                const dynamicColors = {};\n                const combinedColorsRegex = new RegExp(Object.values(weUtils.DEFAULT_PALETTE).join('|'), 'gi');\n                svg = svg.replace(combinedColorsRegex, match => {\n                    const colorId = Object.keys(weUtils.DEFAULT_PALETTE).find(key => weUtils.DEFAULT_PALETTE[key] === match.toUpperCase());\n                    const colorKey = 'c' + colorId\n                    dynamicColors[colorKey] = weUtils.getCSSVariableValue('o-color-' + colorId);\n                    return dynamicColors[colorKey];\n                });\n                const fileName = mediaUrl.split('/').pop();\n                const file = new File([svg], fileName, {\n                    type: \"image/svg+xml\",\n                });\n                imgEl.src = URL.createObjectURL(file);\n                if (Object.keys(dynamicColors).length) {\n                    media.isDynamicSVG = true;\n                    media.dynamicColors = dynamicColors;\n                }\n            }\n        } catch {\n            console.error('CORS is misconfigured on the API server, image will be treated as non-dynamic.');\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useService, useChildRef } from '@web/core/utils/hooks';\nimport { Mutex } from \"@web/core/utils/concurrency\";\nimport { Dialog } from '@web/core/dialog/dialog';\nimport { Notebook } from '@web/core/notebook/notebook';\nimport { ImageSelector } from './image_selector';\nimport { DocumentSelector } from './document_selector';\nimport { IconSelector } from './icon_selector';\nimport { VideoSelector } from './video_selector';\n\nimport { Component, useState, useRef, useEffect } from \"@odoo/owl\";\n\nexport const TABS = {\n    IMAGES: {\n        id: 'IMAGES',\n        title: _t(\"Images\"),\n        Component: ImageSelector,\n    },\n    DOCUMENTS: {\n        id: 'DOCUMENTS',\n        title: _t(\"Documents\"),\n        Component: DocumentSelector,\n    },\n    ICONS: {\n        id: 'ICONS',\n        title: _t(\"Icons\"),\n        Component: IconSelector,\n    },\n    VIDEOS: {\n        id: 'VIDEOS',\n        title: _t(\"Videos\"),\n        Component: VideoSelector,\n    },\n};\n\nexport class MediaDialog extends Component {\n    static template = \"web_editor.MediaDialog\";\n    static defaultProps = {\n        useMediaLibrary: true,\n    };\n    static components = {\n        ...Object.keys(TABS).map((key) => TABS[key].Component),\n        Dialog,\n        Notebook,\n    };\n    static props = [\"*\"];\n\n    setup() {\n        this.size = 'xl';\n        this.contentClass = 'o_select_media_dialog h-100';\n        this.modalRef = useChildRef();\n\n        this.orm = useService('orm');\n        this.notificationService = useService('notification');\n        this.mutex = new Mutex();\n\n        this.tabs = [];\n        this.selectedMedia = useState({});\n\n        this.addButtonRef = useRef('add-button');\n\n        this.initialIconClasses = [];\n\n        this.addTabs();\n        this.errorMessages = {};\n\n        this.state = useState({\n            activeTab: this.initialActiveTab,\n        });\n\n        useEffect(\n            (nbSelectedAttachments) => {\n                // Disable/enable the add button depending on whether some media\n                // are selected or not.\n                this.addButtonRef.el.toggleAttribute(\"disabled\", !nbSelectedAttachments);\n            },\n            () => [this.selectedMedia[this.state.activeTab].length]\n        );\n    }\n\n    get initialActiveTab() {\n        if (this.props.activeTab) {\n            return this.props.activeTab;\n        }\n        if (this.props.media) {\n            const correspondingTab = Object.keys(TABS).find(id => TABS[id].Component.tagNames.includes(this.props.media.tagName));\n            if (correspondingTab) {\n                return correspondingTab;\n            }\n        }\n        return this.tabs[0].id;\n    }\n\n    addTab(tab, additionalProps = {}) {\n        this.selectedMedia[tab.id] = [];\n        this.tabs.push({\n            ...tab,\n            props: {\n                ...tab.props,\n                ...additionalProps,\n                id: tab.id,\n                resModel: this.props.resModel,\n                resId: this.props.resId,\n                media: this.props.media,\n                multiImages: this.props.multiImages,\n                selectedMedia: this.selectedMedia,\n                selectMedia: (...args) => this.selectMedia(...args, tab.id, additionalProps.multiSelect),\n                save: this.save.bind(this),\n                onAttachmentChange: this.props.onAttachmentChange,\n                errorMessages: (errorMessage) => this.errorMessages[tab.id] = errorMessage,\n                modalRef: this.modalRef,\n            },\n        });\n    }\n\n    addTabs() {\n        const onlyImages = this.props.onlyImages || this.props.multiImages || (this.props.media && this.props.media.parentElement && (this.props.media.parentElement.dataset.oeField === 'image' || this.props.media.parentElement.dataset.oeType === 'image'));\n        const noDocuments = onlyImages || this.props.noDocuments;\n        const noIcons = onlyImages || this.props.noIcons;\n        const noVideos = onlyImages || this.props.noVideos;\n\n        if (!this.props.noImages) {\n            this.addTab(TABS.IMAGES, {\n                useMediaLibrary: this.props.useMediaLibrary,\n                multiSelect: this.props.multiImages,\n            });\n        }\n        if (!noDocuments) {\n            this.addTab(TABS.DOCUMENTS);\n        }\n        if (!noIcons) {\n            const fonts = TABS.ICONS.Component.initFonts();\n            this.addTab(TABS.ICONS, {\n                fonts,\n            });\n\n            if (this.props.media && TABS.ICONS.Component.tagNames.includes(this.props.media.tagName)) {\n                const classes = this.props.media.className.split(/\\s+/);\n                const mediaFont = fonts.find(font => classes.includes(font.base));\n                if (mediaFont) {\n                    const selectedIcon = mediaFont.icons.find(icon => icon.names.some(name => classes.includes(name)));\n                    if (selectedIcon) {\n                        this.initialIconClasses.push(...selectedIcon.names);\n                        this.selectMedia(selectedIcon, TABS.ICONS.id);\n                    }\n                }\n            }\n        }\n        if (!noVideos) {\n            this.addTab(TABS.VIDEOS, {\n                vimeoPreviewIds: this.props.vimeoPreviewIds,\n                isForBgVideo: this.props.isForBgVideo,\n            });\n        }\n    }\n\n    /**\n     * Render the selected media for insertion in the editor\n     *\n     * @param {Array<Object>} selectedMedia\n     * @returns {Array<HTMLElement>}\n     */\n    async renderMedia(selectedMedia) {\n        // Calling a mutex to make sure RPC calls inside `createElements` are\n        // properly awaited (e.g. avoid creating multiple attachments when\n        // clicking multiple times on the same media). As `createElements` is\n        // static, the mutex has to be set on the media dialog itself to be\n        // destroyed with its instance.\n        const elements = await this.mutex.exec(async() =>\n            await TABS[this.state.activeTab].Component.createElements(selectedMedia, { orm: this.orm })\n        );\n        elements.forEach(element => {\n            if (this.props.media) {\n                element.classList.add(...this.props.media.classList);\n                const style = this.props.media.getAttribute('style');\n                if (style) {\n                    element.setAttribute('style', style);\n                }\n                if (this.state.activeTab === TABS.IMAGES.id) {\n                    if (this.props.media.dataset.shape) {\n                        element.dataset.shape = this.props.media.dataset.shape;\n                    }\n                    if (this.props.media.dataset.shapeColors) {\n                        element.dataset.shapeColors = this.props.media.dataset.shapeColors;\n                    }\n                    if (this.props.media.dataset.shapeFlip) {\n                        element.dataset.shapeFlip = this.props.media.dataset.shapeFlip;\n                    }\n                    if (this.props.media.dataset.shapeRotate) {\n                        element.dataset.shapeRotate = this.props.media.dataset.shapeRotate;\n                    }\n                    if (this.props.media.dataset.hoverEffect) {\n                        element.dataset.hoverEffect = this.props.media.dataset.hoverEffect;\n                    }\n                    if (this.props.media.dataset.hoverEffectColor) {\n                        element.dataset.hoverEffectColor = this.props.media.dataset.hoverEffectColor;\n                    }\n                    if (this.props.media.dataset.hoverEffectStrokeWidth) {\n                        element.dataset.hoverEffectStrokeWidth = this.props.media.dataset.hoverEffectStrokeWidth;\n                    }\n                    if (this.props.media.dataset.hoverEffectIntensity) {\n                        element.dataset.hoverEffectIntensity = this.props.media.dataset.hoverEffectIntensity;\n                    }\n                    if (this.props.media.dataset.shapeAnimationSpeed) {\n                        element.dataset.shapeAnimationSpeed = this.props.media.dataset.shapeAnimationSpeed;\n                    }\n                }\n            }\n            for (const otherTab of Object.keys(TABS).filter(key => key !== this.state.activeTab)) {\n                for (const property of TABS[otherTab].Component.mediaSpecificStyles) {\n                    element.style.removeProperty(property);\n                }\n                element.classList.remove(...TABS[otherTab].Component.mediaSpecificClasses);\n                const extraClassesToRemove = [];\n                for (const name of TABS[otherTab].Component.mediaExtraClasses) {\n                    if (typeof(name) === 'string') {\n                        extraClassesToRemove.push(name);\n                    } else { // Regex\n                        for (const className of element.classList) {\n                            if (className.match(name)) {\n                                extraClassesToRemove.push(className);\n                            }\n                        }\n                    }\n                }\n                // Remove classes that do not also exist in the target type.\n                element.classList.remove(...extraClassesToRemove.filter(candidateName => {\n                    for (const name of TABS[this.state.activeTab].Component.mediaExtraClasses) {\n                        if (typeof(name) === 'string') {\n                            if (candidateName === name) {\n                                return false;\n                            }\n                        } else { // Regex\n                            for (const className of element.classList) {\n                                if (className.match(candidateName)) {\n                                    return false;\n                                }\n                            }\n                        }\n                    }\n                    return true;\n                }));\n            }\n            element.classList.remove(...this.initialIconClasses);\n            element.classList.remove('o_modified_image_to_save');\n            element.classList.remove('oe_edited_link');\n            element.classList.add(...TABS[this.state.activeTab].Component.mediaSpecificClasses);\n        });\n        return elements;\n    }\n\n    selectMedia(media, tabId, multiSelect) {\n        if (multiSelect) {\n            const isMediaSelected = this.selectedMedia[tabId].map(({ id }) => id).includes(media.id);\n            if (!isMediaSelected) {\n                this.selectedMedia[tabId].push(media);\n            } else {\n                this.selectedMedia[tabId] = this.selectedMedia[tabId].filter(m => m.id !== media.id);\n            }\n        } else {\n            this.selectedMedia[tabId] = [media];\n        }\n    }\n\n    async save() {\n        if (this.errorMessages[this.state.activeTab]) {\n            this.notificationService.add(this.errorMessages[this.state.activeTab], {\n                type: 'danger',\n            });\n            return;\n        }\n        const selectedMedia = this.selectedMedia[this.state.activeTab];\n        // TODO In master: clean the save method so it performs the specific\n        // adaptation before saving from the active media selector and find a\n        // way to simply close the dialog if the media element remains the same.\n        const saveSelectedMedia = selectedMedia.length\n            && (this.state.activeTab !== TABS.ICONS.id || selectedMedia[0].initialIconChanged || !this.props.media);\n        if (saveSelectedMedia) {\n            const elements = await this.renderMedia(selectedMedia);\n            if (this.props.multiImages) {\n                this.props.save(elements);\n            } else {\n                this.props.save(elements[0]);\n            }\n        }\n        this.props.close();\n    }\n\n    onTabChange(tab) {\n        this.state.activeTab = tab;\n    }\n}\n", "/** @odoo-module **/\n\nimport { useDebounced } from '@web/core/utils/timing';\nimport { useAutofocus } from '@web/core/utils/hooks';\n\nimport { Component, xml, useEffect, useState } from \"@odoo/owl\";\n\nexport class SearchMedia extends Component {\n    static template = xml`\n        <div class=\"position-relative mw-lg-25 flex-grow-1 me-auto\">\n            <input type=\"text\" class=\"o_we_search o_input form-control\" t-att-placeholder=\"props.searchPlaceholder.trim()\" t-model=\"state.input\" t-ref=\"autofocus\"/>\n            <i class=\"oi oi-search input-group-text position-absolute end-0 top-50 me-n3 px-2 py-1 translate-middle bg-transparent border-0\" title=\"Search\" role=\"img\" aria-label=\"Search\"/>\n        </div>`;\n    static props = [\"searchPlaceholder\", \"search\", \"needle\"];\n    setup() {\n        useAutofocus();\n        this.debouncedSearch = useDebounced(this.props.search, 1000);\n\n        this.state = useState({\n            input: this.props.needle || '',\n        });\n\n        useEffect((input) => {\n            // Do not trigger a search on the initial render.\n            if (this.hasRendered) {\n                this.debouncedSearch(input);\n            } else {\n                this.hasRendered = true;\n            }\n        }, () => [this.state.input]);\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useAutofocus, useService } from '@web/core/utils/hooks';\nimport { debounce } from '@web/core/utils/timing';\n\nimport { Component, useState, useRef, onMounted, onWillStart } from \"@odoo/owl\";\n\nclass VideoOption extends Component {\n    static template = \"web_editor.VideoOption\";\n    static props = {\n        description: {type: String, optional: true},\n        label: {type: String, optional: true},\n        onChangeOption: Function,\n        value: {type: Boolean, optional: true},\n    };\n}\n\nclass VideoIframe extends Component {\n    static template = \"web_editor.VideoIframe\";\n    static props = {\n        src: { type: String },\n    };\n}\n\nexport class VideoSelector extends Component {\n    static mediaSpecificClasses = [\"media_iframe_video\"];\n    static mediaSpecificStyles = [];\n    static mediaExtraClasses = [];\n    static tagNames = [\"IFRAME\", \"DIV\"];\n    static template = \"web_editor.VideoSelector\";\n    static components = {\n        VideoIframe,\n        VideoOption,\n    };\n    static props = {\n        selectMedia: Function,\n        errorMessages: Function,\n        vimeoPreviewIds: {type: Array, optional: true},\n        isForBgVideo: {type: Boolean, optional: true},\n        media: {type: Object, optional: true},\n        \"*\": true,\n    };\n    static defaultProps = {\n        vimeoPreviewIds: [],\n        isForBgVideo: false,\n    };\n\n    setup() {\n        this.http = useService('http');\n\n        this.PLATFORMS = {\n            youtube: 'youtube',\n            dailymotion: 'dailymotion',\n            vimeo: 'vimeo',\n            youku: 'youku',\n        };\n\n        this.OPTIONS = {\n            autoplay: {\n                label: _t(\"Autoplay\"),\n                description: _t(\"Videos are muted when autoplay is enabled\"),\n                platforms: [this.PLATFORMS.youtube, this.PLATFORMS.dailymotion, this.PLATFORMS.vimeo],\n                urlParameter: 'autoplay=1',\n            },\n            loop: {\n                label: _t(\"Loop\"),\n                platforms: [this.PLATFORMS.youtube, this.PLATFORMS.vimeo],\n                urlParameter: 'loop=1',\n            },\n            hide_controls: {\n                label: _t(\"Hide player controls\"),\n                platforms: [this.PLATFORMS.youtube, this.PLATFORMS.dailymotion, this.PLATFORMS.vimeo],\n                urlParameter: 'controls=0',\n            },\n            hide_fullscreen: {\n                label: _t(\"Hide fullscreen button\"),\n                platforms: [this.PLATFORMS.youtube],\n                urlParameter: 'fs=0',\n                isHidden: () => this.state.options.filter(option => option.id === 'hide_controls')[0].value,\n            },\n            hide_dm_logo: {\n                label: _t(\"Hide Dailymotion logo\"),\n                platforms: [this.PLATFORMS.dailymotion],\n                urlParameter: 'ui-logo=0',\n            },\n            hide_dm_share: {\n                label: _t(\"Hide sharing button\"),\n                platforms: [this.PLATFORMS.dailymotion],\n                urlParameter: 'sharing-enable=0',\n            },\n        };\n\n        this.state = useState({\n            options: [],\n            src: '',\n            urlInput: '',\n            platform: null,\n            vimeoPreviews: [],\n            errorMessage: '',\n        });\n        this.urlInputRef = useRef('url-input');\n\n        onWillStart(async () => {\n            if (this.props.media) {\n                const src = this.props.media.dataset.oeExpression || this.props.media.dataset.src || (this.props.media.tagName === 'IFRAME' && this.props.media.getAttribute('src')) || '';\n                if (src) {\n                    this.state.urlInput = src;\n                    await this.updateVideo();\n\n                    this.state.options = this.state.options.map((option) => {\n                        const { urlParameter } = this.OPTIONS[option.id];\n                        return { ...option, value: src.indexOf(urlParameter) >= 0 };\n                    });\n                }\n            }\n        });\n\n        onMounted(async () => {\n            await Promise.all(this.props.vimeoPreviewIds.map(async (videoId) => {\n                const { thumbnail_url: thumbnailSrc } = await this.http.get(`https://vimeo.com/api/oembed.json?url=http%3A//vimeo.com/${encodeURIComponent(videoId)}`);\n                this.state.vimeoPreviews.push({\n                    id: videoId,\n                    thumbnailSrc,\n                    src: `https://player.vimeo.com/video/${encodeURIComponent(videoId)}`\n                });\n            }));\n        });\n\n        useAutofocus();\n\n        this.onChangeUrl = debounce((ev) => this.updateVideo(ev.target.value), 500);\n    }\n\n    get shownOptions() {\n        if (this.props.isForBgVideo) {\n            return [];\n        }\n        return this.state.options.filter(option => !this.OPTIONS[option.id].isHidden || !this.OPTIONS[option.id].isHidden());\n    }\n\n    async onChangeOption(optionId) {\n        this.state.options = this.state.options.map(option => {\n            if (option.id === optionId) {\n                return { ...option, value: !option.value };\n            }\n            return option;\n        });\n        await this.updateVideo();\n    }\n\n    async onClickSuggestion(src) {\n        this.state.urlInput = src;\n        await this.updateVideo();\n    }\n\n    async updateVideo() {\n        if (!this.state.urlInput) {\n            this.state.src = '';\n            this.state.urlInput = '';\n            this.state.options = [];\n            this.state.platform = null;\n            this.state.errorMessage = '';\n            /**\n             * When the url input is emptied, we need to call the `selectMedia`\n             * callback function to notify the other components that the media\n             * has changed.\n             */\n            this.props.selectMedia({});\n            return;\n        }\n\n        // Detect if we have an embed code rather than an URL\n        const embedMatch = this.state.urlInput.match(/(src|href)=[\"']?([^\"']+)?/);\n        if (embedMatch && embedMatch[2].length > 0 && embedMatch[2].indexOf('instagram')) {\n            embedMatch[1] = embedMatch[2]; // Instagram embed code is different\n        }\n        const url = embedMatch ? embedMatch[1] : this.state.urlInput;\n\n        const options = {};\n        if (this.props.isForBgVideo) {\n            Object.keys(this.OPTIONS).forEach(key => {\n                options[key] = true;\n            });\n        } else {\n            for (const option of this.shownOptions) {\n                options[option.id] = option.value;\n            }\n        }\n\n        const {\n            embed_url: src,\n            video_id: videoId,\n            params,\n            platform\n        } = await this._getVideoURLData(url, options);\n\n        if (!src) {\n            this.state.errorMessage = _t(\"The provided url is not valid\");\n        } else if (!platform) {\n            this.state.errorMessage =\n                _t(\"The provided url does not reference any supported video\");\n        } else {\n            this.state.errorMessage = '';\n        }\n        this.props.errorMessages(this.state.errorMessage);\n\n        const newOptions = [];\n        if (platform && platform !== this.state.platform) {\n            Object.keys(this.OPTIONS).forEach(key => {\n                if (this.OPTIONS[key].platforms.includes(platform)) {\n                    const { label, description } = this.OPTIONS[key];\n                    newOptions.push({ id: key, label, description });\n                }\n            });\n        }\n\n        this.state.src = src;\n        this.props.selectMedia({\n            id: src,\n            src,\n            platform,\n            videoId,\n            params\n        });\n        if (platform !== this.state.platform) {\n            this.state.platform = platform;\n            this.state.options = newOptions;\n        }\n    }\n\n    /**\n     * Keep rpc call in distinct method make it patchable by test.\n     */\n    async _getVideoURLData(url, options) {\n        return await rpc('/web_editor/video_url/data', {\n            video_url: url,\n            ...options,\n        });\n    }\n\n    /**\n     * Utility method, called by the MediaDialog component.\n     */\n    static createElements(selectedMedia) {\n        return selectedMedia.map(video => {\n            const div = document.createElement('div');\n            div.dataset.oeExpression = video.src;\n            div.innerHTML = `\n                <div class=\"css_editable_mode_display\"></div>\n                <div class=\"media_iframe_video_size\" contenteditable=\"false\"></div>\n                <iframe loading=\"lazy\" frameborder=\"0\" contenteditable=\"false\" allowfullscreen=\"allowfullscreen\"></iframe>\n            `;\n            div.querySelector('iframe').src = video.src;\n            return div;\n        });\n    }\n}\n", "/** @odoo-module */\nimport { useService } from '@web/core/utils/hooks';\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class ProgressBar extends Component {\n    static template = \"web_editor.ProgressBar\";\n    static props = {\n        progress: { type: Number, optional: true },\n        hasError: { type: Boolean, optional: true },\n        uploaded: { type: Boolean, optional: true },\n        name: String,\n        size: { type: String, optional: true },\n        errorMessage: { type: String, optional: true },\n    };\n    static defaultProps = {\n        progress: 0,\n        hasError: false,\n        uploaded: false,\n        size: \"\",\n        errorMessage: \"\",\n    };\n\n    get progress() {\n        return Math.round(this.props.progress);\n    }\n}\n\nexport class UploadProgressToast extends Component {\n    static template = \"web_editor.UploadProgressToast\";\n    static components = {\n        ProgressBar,\n    };\n    static props = {\n        close: Function,\n    };\n\n    setup() {\n        this.uploadService = useService('upload');\n\n        this.state = useState(this.uploadService.progressToast);\n    }\n}\n", "/** @odoo-module **/\n\nimport { rpc } from '@web/core/network/rpc';\nimport { registry } from '@web/core/registry';\nimport { UploadProgressToast } from './upload_progress_toast';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { checkFileSize } from \"@web/core/utils/files\";\nimport { humanNumber } from \"@web/core/utils/numbers\";\nimport { getDataURLFromFile } from \"@web/core/utils/urls\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { reactive } from \"@odoo/owl\";\n\nexport const AUTOCLOSE_DELAY = 3000;\nexport const AUTOCLOSE_DELAY_LONG = 8000;\n\nexport const uploadService = {\n    dependencies: ['notification'],\n    start(env, { notification }) {\n        let fileId = 0;\n        const progressToast = reactive({\n            files: {},\n            isVisible: false,\n        });\n\n        registry.category('main_components').add('UploadProgressToast', {\n            Component: UploadProgressToast,\n            props: {\n                close: () => progressToast.isVisible = false,\n            }\n        });\n\n        const addFile = (file) => {\n            progressToast.files[file.id] = file;\n            progressToast.isVisible = true;\n            return progressToast.files[file.id];\n        };\n\n        const deleteFile = (fileId) => {\n            delete progressToast.files[fileId];\n            if (!Object.keys(progressToast.files).length) {\n                progressToast.isVisible = false;\n            }\n        };\n        return {\n            get progressToast() {\n                return progressToast;\n            },\n            get fileId() {\n                return fileId;\n            },\n            addFile,\n            deleteFile,\n            incrementId() {\n                fileId++;\n            },\n            uploadUrl: async (url, { resModel, resId }, onUploaded) => {\n                const attachment = await rpc('/web_editor/attachment/add_url', {\n                    url,\n                    'res_model': resModel,\n                    'res_id': resId,\n                });\n                await onUploaded(attachment);\n            },\n            /**\n             * This takes an array of files (from an input HTMLElement), and\n             * uploads them while managing the UploadProgressToast.\n             *\n             * @param {Array<File>} files\n             * @param {Object} options\n             * @param {Function} onUploaded\n             */\n            uploadFiles: async (files, {resModel, resId, isImage}, onUploaded) => {\n                // Upload the smallest file first to block the user the least possible.\n                const sortedFiles = Array.from(files).sort((a, b) => a.size - b.size);\n                for (const file of sortedFiles) {\n                    let fileSize = file.size;\n                    if (!checkFileSize(fileSize, notification)) {\n                        return null;\n                    }\n                    if (!fileSize) {\n                        fileSize = \"\";\n                    } else {\n                        fileSize = humanNumber(fileSize) + \"B\";\n                    }\n\n                    const id = ++fileId;\n                    file.progressToastId = id;\n                    // This reactive object, built based on the files array,\n                    // is given as a prop to the UploadProgressToast.\n                    addFile({\n                        id,\n                        name: file.name,\n                        size: fileSize,\n                    });\n                }\n\n                // Upload one file at a time: no need to parallel as upload is\n                // limited by bandwidth.\n                for (const sortedFile of sortedFiles) {\n                    const file = progressToast.files[sortedFile.progressToastId];\n                    let dataURL;\n                    try {\n                        dataURL = await getDataURLFromFile(sortedFile);\n                    } catch {\n                        deleteFile(file.id);\n                        env.services.notification.add(\n                            sprintf(\n                                _t('Could not load the file \"%s\".'),\n                                sortedFile.name\n                            ),\n                            { type: 'danger' }\n                        );\n                        continue\n                    }\n                    try {\n                        const xhr = new XMLHttpRequest();\n                        xhr.upload.addEventListener('progress', ev => {\n                            const rpcComplete = ev.loaded / ev.total * 100;\n                            file.progress = rpcComplete;\n                        });\n                        xhr.upload.addEventListener('load', function () {\n                            // Don't show yet success as backend code only starts now\n                            file.progress = 100;\n                        });\n                        const attachment = await rpc('/web_editor/attachment/add_data', {\n                            'name': file.name,\n                            'data': dataURL.split(',')[1],\n                            'res_id': resId,\n                            'res_model': resModel,\n                            'is_image': !!isImage,\n                            'width': 0,\n                            'quality': 0,\n                        }, {xhr});\n                        if (attachment.error) {\n                            file.hasError = true;\n                            file.errorMessage = attachment.error;\n                        } else {\n                            if (attachment.mimetype === 'image/webp') {\n                                // Generate alternate format for reports.\n                                const image = document.createElement('img');\n                                image.src = `data:image/webp;base64,${dataURL.split(',')[1]}`;\n                                await new Promise(resolve => image.addEventListener('load', resolve));\n                                const canvas = document.createElement('canvas');\n                                canvas.width = image.width;\n                                canvas.height = image.height;\n                                const ctx = canvas.getContext('2d');\n                                ctx.fillStyle = 'rgb(255, 255, 255)';\n                                ctx.fillRect(0, 0, canvas.width, canvas.height);\n                                ctx.drawImage(image, 0, 0);\n                                const altDataURL = canvas.toDataURL('image/jpeg', 0.75);\n                                await rpc('/web_editor/attachment/add_data', {\n                                    'name': file.name.replace(/\\.webp$/, '.jpg'),\n                                    'data': altDataURL.split(',')[1],\n                                    'res_id': attachment.id,\n                                    'res_model': 'ir.attachment',\n                                    'is_image': true,\n                                    'width': 0,\n                                    'quality': 0,\n                                }, {xhr});\n                            }\n                            file.uploaded = true;\n                            await onUploaded(attachment);\n                        }\n                        // If there's an error, display the error message for longer\n                        let message_autoclose_delay = file.hasError ? AUTOCLOSE_DELAY_LONG : AUTOCLOSE_DELAY;\n                        setTimeout(() => deleteFile(file.id), message_autoclose_delay);\n                    } catch (error) {\n                        file.hasError = true;\n                        setTimeout(() => deleteFile(file.id), AUTOCLOSE_DELAY_LONG);\n                        throw error;\n                    }\n                }\n            }\n        };\n    },\n};\n\n// registry.category('services').add('upload', uploadService);\n", "import { patch } from \"@web/core/utils/patch\";\nimport { FileDocumentsSelector } from \"@html_editor/main/media/media_dialog/file_documents_selector\";\n\npatch(FileDocumentsSelector.prototype, {\n    get attachmentsDomain() {\n        const domain = super.attachmentsDomain;\n        domain.push(\"|\", [\"url\", \"=\", false], \"!\", [\"url\", \"=like\", \"/web/image/website.%\"]);\n        domain.push([\"key\", \"=\", false]);\n        return domain;\n    },\n});\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { ImageSelector } from '@web_editor/components/media_dialog/image_selector';\nimport { ImageSelector as HtmlImageSelector } from \"@html_editor/main/media/media_dialog/image_selector\";\n\npatch(ImageSelector.prototype, {\n    get attachmentsDomain() {\n        const domain = super.attachmentsDomain;\n        domain.push('|', ['url', '=', false], '!', ['url', '=like', '/web/image/website.%']);\n        domain.push(['key', '=', false]);\n        return domain;\n    }\n});\n\npatch(HtmlImageSelector.prototype, {\n    get attachmentsDomain() {\n        const domain = super.attachmentsDomain;\n        domain.push('|', ['url', '=', false], '!', ['url', '=like', '/web/image/website.%']);\n        domain.push(['key', '=', false]);\n        return domain;\n    }\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { MediaDialog, TABS } from \"@web_editor/components/media_dialog/media_dialog\";\nimport { ImageSelector } from \"@web_editor/components/media_dialog/image_selector\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { UnsplashError } from \"../unsplash_error/unsplash_error\";\n\npatch(ImageSelector.prototype, {\n    setup() {\n        super.setup();\n        this.unsplash = useService('unsplash');\n        this.keepLastUnsplash = new KeepLast();\n\n        this.state.unsplashRecords = [];\n        this.state.isFetchingUnsplash = false;\n        this.state.isMaxed = false;\n        this.state.unsplashError = null;\n        this.state.useUnsplash = true;\n        this.NUMBER_OF_RECORDS_TO_DISPLAY = 30;\n\n        this.errorMessages = {\n            'key_not_found': {\n                title: _t(\"Setup Unsplash to access royalty free photos.\"),\n                subtitle: \"\",\n            },\n            401: {\n                title: _t(\"Unauthorized Key\"),\n                subtitle: _t(\"Please check your Unsplash access key and application ID.\"),\n            },\n            403: {\n                title: _t(\"Search is temporarily unavailable\"),\n                subtitle: _t(\"The max number of searches is exceeded. Please retry in an hour or extend to a better account.\"),\n            },\n        };\n    },\n\n    get canLoadMore() {\n        if (this.state.searchService === 'all') {\n            return super.canLoadMore || this.state.needle && !this.state.isMaxed && !this.state.unsplashError;\n        } else if (this.state.searchService === 'unsplash') {\n            return this.state.needle && !this.state.isMaxed && !this.state.unsplashError;\n        }\n        return super.canLoadMore;\n    },\n\n    get hasContent() {\n        if (this.state.searchService === 'all') {\n            return super.hasContent || !!this.state.unsplashRecords.length;\n        } else if (this.state.searchService === 'unsplash') {\n            return !!this.state.unsplashRecords.length;\n        }\n        return super.hasContent;\n    },\n\n    get errorTitle() {\n        if (this.errorMessages[this.state.unsplashError]) {\n            return this.errorMessages[this.state.unsplashError].title;\n        }\n        return _t(\"Something went wrong\");\n    },\n\n    get errorSubtitle() {\n        if (this.errorMessages[this.state.unsplashError]) {\n            return this.errorMessages[this.state.unsplashError].subtitle;\n        }\n        return _t(\"Please check your internet connection or contact administrator.\");\n    },\n\n    get selectedRecordIds() {\n        return this.props.selectedMedia[this.props.id].filter(media => media.mediaType === 'unsplashRecord').map(({ id }) => id);\n    },\n\n    get isFetching() {\n        return super.isFetching || this.state.isFetchingUnsplash;\n    },\n\n    get combinedRecords() {\n        /**\n         * Creates an array with alternating elements from two arrays.\n         *\n         * @param {Array} a\n         * @param {Array} b\n         * @returns {Array} alternating elements from a and b, starting with\n         *     an element of a\n         */\n        function alternate(a, b) {\n            return [\n                a.map((v, i) => i < b.length ? [v, b[i]] : v),\n                b.slice(a.length),\n            ].flat(2);\n        }\n        return alternate(this.state.unsplashRecords, this.state.libraryMedia);\n    },\n\n    get allAttachments() {\n        return [...super.allAttachments, ...this.state.unsplashRecords];\n    },\n\n    // It seems that setters are mandatory when patching a component that\n    // extends another component.\n    set canLoadMore(_) {},\n    set hasContent(_) {},\n    set isFetching(_) {},\n    set selectedMediaIds(_) {},\n    set attachmentsDomain(_) {},\n    set errorTitle(_) {},\n    set errorSubtitle(_) {},\n    set selectedRecordIds(_) {},\n\n    async fetchUnsplashRecords(offset) {\n        if (!this.state.needle) {\n            return { records: [], isMaxed: false };\n        }\n        this.state.isFetchingUnsplash = true;\n        try {\n            const { isMaxed, images } = await this.unsplash.getImages(this.state.needle, offset, this.NUMBER_OF_RECORDS_TO_DISPLAY, this.props.orientation);\n            this.state.isFetchingUnsplash = false;\n            this.state.unsplashError = false;\n            // Ignore duplicates.\n            const existingIds = this.state.unsplashRecords.map(existing => existing.id);\n            const newImages = images.filter(record => !existingIds.includes(record.id));\n            const records = newImages.map(record => {\n                const url = new URL(record.urls.regular);\n                // In small windows, row height could get quite a bit larger than the min, so we keep some leeway.\n                url.searchParams.set('h', 2 * this.MIN_ROW_HEIGHT);\n                url.searchParams.delete('w');\n                return Object.assign({}, record, {\n                    url: url.toString(),\n                    mediaType: 'unsplashRecord',\n                });\n            });\n            return { isMaxed, records };\n        } catch (e) {\n            this.state.isFetchingUnsplash = false;\n            if (e === 'no_access') {\n                this.state.useUnsplash = false;\n            } else {\n                this.state.unsplashError = e;\n            }\n            return { records: [], isMaxed: true };\n        }\n    },\n\n    async loadMore(...args) {\n        await super.loadMore(...args);\n        return this.keepLastUnsplash.add(this.fetchUnsplashRecords(this.state.unsplashRecords.length)).then(({ records, isMaxed }) => {\n            // This is never reached if another search or loadMore occurred.\n            this.state.unsplashRecords.push(...records);\n            this.state.isMaxed = isMaxed;\n        });\n    },\n\n    async search(...args) {\n        await super.search(...args);\n        await this.searchUnsplash();\n    },\n\n    async searchUnsplash() {\n        if (!this.state.needle) {\n            this.state.unsplashError = false;\n            this.state.unsplashRecords = [];\n            this.state.isMaxed = false;\n        }\n        return this.keepLastUnsplash.add(this.fetchUnsplashRecords(0)).then(({ records, isMaxed }) => {\n            // This is never reached if a new search occurred.\n            this.state.unsplashRecords = records;\n            this.state.isMaxed = isMaxed;\n        });\n    },\n\n    async onClickRecord(media) {\n        this.props.selectMedia({ ...media, mediaType: 'unsplashRecord', query: this.state.needle });\n        if (!this.props.multiSelect) {\n            await this.props.save();\n        }\n    },\n\n    async submitCredentials(key, appId) {\n        this.state.unsplashError = null;\n        await rpc('/web_unsplash/save_unsplash', { key, appId });\n        await this.searchUnsplash();\n    },\n});\nImageSelector.components = {\n    ...ImageSelector.components,\n    UnsplashError,\n};\n\npatch(MediaDialog.prototype, {\n    setup() {\n        super.setup();\n\n        this.unsplashService = useService('unsplash');\n    },\n\n    async save() {\n        const selectedImages = this.selectedMedia[TABS.IMAGES.id];\n        if (selectedImages) {\n            const unsplashRecords = selectedImages.filter(media => media.mediaType === 'unsplashRecord');\n            if (unsplashRecords.length) {\n                await this.unsplashService.uploadUnsplashRecords(unsplashRecords, { resModel: this.props.resModel, resId: this.props.resId }, (attachments) => {\n                    this.selectedMedia[TABS.IMAGES.id] = this.selectedMedia[TABS.IMAGES.id].filter(media => media.mediaType !== 'unsplashRecord');\n                    this.selectedMedia[TABS.IMAGES.id] = this.selectedMedia[TABS.IMAGES.id].concat(attachments.map(attachment => ({...attachment, mediaType: 'attachment'})));\n                });\n            }\n        }\n        return super.save(...arguments);\n    },\n});\n", "/** @odoo-module **/\n\n// Redefine the getRangeAt function in order to avoid an error appearing\n// sometimes when an input element is focused on Firefox.\n// The error happens because the range returned by getRangeAt is \"restricted\".\n// Ex: Range { commonAncestorContainer: Restricted, startContainer: Restricted,\n// startOffset: 0, endContainer: Restricted, endOffset: 0, collapsed: true }\n// The solution consists in detecting when the range is restricted and then\n// redefining it manually based on the current selection.\nconst originalGetRangeAt = Selection.prototype.getRangeAt;\nSelection.prototype.getRangeAt = function () {\n    let range = originalGetRangeAt.apply(this, arguments);\n    // Check if the range is restricted\n    if (range.startContainer && !Object.getPrototypeOf(range.startContainer)) {\n        // Define the range manually based on the selection\n        range = document.createRange();\n        range.setStart(this.anchorNode, 0);\n        range.setEnd(this.focusNode, 0);\n    }\n    return range;\n};\n", "/** @odoo-module **/\n\nexport const ColumnLayoutMixin = {\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     * @returns {integer|string} number of columns or \"custom\"\n     */\n    _getNbColumns(columnEls, isMobile) {\n        if (!columnEls) {\n            return 0;\n        }\n        if (this._areColsCustomized(columnEls, isMobile)) {\n            return \"custom\";\n        }\n\n        const resolutionModifier = isMobile ? \"\" : \"lg-\";\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 = this._getFirstItem(columnEls, isMobile).classList\n            .contains(`offset-${resolutionModifier}1`) ? 1 : 0;\n\n        return Math.floor((12 - offsetSize) / colSize);\n    },\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     */\n    _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     */\n    _addMobileOrders(columnEls) {\n        for (let i = 0; i < columnEls.length; i++) {\n            columnEls[i].style.order = i;\n            columnEls[i].classList.add(\"order-lg-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     */\n    _removeMobileOrders(columnEls) {\n        for (const el of columnEls) {\n            el.style.order = \"\";\n            el.classList.remove(\"order-lg-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     * @returns {boolean}\n     */\n    _areColsCustomized(columnEls, isMobile) {\n        const resolutionModifier = isMobile ? \"\" : \"lg-\";\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((columnEl) =>\n            parseInt(columnEl.className.match(colRegex)?.[1] || 12) === colSize);\n        if (!allColsSizesEqual) {\n            return true;\n        }\n        const offsetRegex = new RegExp(`(?:^|\\\\s+)offset-${resolutionModifier}[1-9][0-1]?(?!\\\\S)`);\n        const nbOffsets = [...columnEls]\n            .filter((columnEl) => columnEl.className.match(offsetRegex)).length;\n        if (nbOffsets === 0) {\n            return false;\n        }\n        if (nbOffsets === 1 && colSize === 2 && this._getFirstItem(columnEls, isMobile).className\n                .match(`offset-${resolutionModifier}1`)) {\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     */\n    _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};\n", "/** @odoo-module **/\n\nimport { renderToElement } from \"@web/core/utils/render\";\nimport {descendants, preserveCursor} from \"@web_editor/js/editor/odoo-editor/src/utils/utils\";\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 {Element} 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: rowGap, rowSize: rowSize, columnGap: columnGap, columnSize: columnSize};\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(el => el !== element\n        && !el.classList.contains(\"o_we_grid_preview\"));\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('web_editor.background_grid', {\n        rowCount: rowCount + 1, rowGap: gridProp.rowGap, rowSize: gridProp.rowSize,\n        columnGap: gridProp.columnGap, 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 work.\n *\n * @private\n * @param {Element} rowEl\n * @param {Element} column\n */\nexport function _gridCleanUp(rowEl, columnEl) {\n    columnEl.style.removeProperty('position');\n    columnEl.style.removeProperty('top');\n    columnEl.style.removeProperty('left');\n    columnEl.style.removeProperty('height');\n    columnEl.style.removeProperty('width');\n    rowEl.style.removeProperty('position');\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 */\nexport function _toggleGridMode(containerEl) {\n    let rowEl = containerEl.querySelector(':scope > .row');\n    const outOfRowEls = [...containerEl.children].filter(el => !el.classList.contains('row'));\n    // Avoid an unwanted rollback that prevents from deleting the text.\n    const avoidRollback = (el) => {\n        for (const node of descendants(el)) {\n            node.ouid = undefined;\n        }\n    };\n    // Keep the text selection.\n    const restoreCursor = !rowEl || outOfRowEls.length > 0 ?\n        preserveCursor(containerEl.ownerDocument) : () => {};\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-lg-12');\n        for (let i = outOfRowEls.length - 1; i >= 0; i--) {\n            columnEl.prepend(outOfRowEls[i]);\n        }\n        avoidRollback(columnEl);\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-lg-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        avoidRollback(columnEl);\n        rowEl.appendChild(columnEl);\n        containerEl.appendChild(rowEl);\n    }\n    restoreCursor();\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) - 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 => {\n        return /^align-items/.test(c);\n    });\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 * @returns {Number}\n */\nfunction _placeColumns(columnEls, rowSize, rowGap, columnSize, columnGap) {\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\n    for (const columnEl of columnEls) {\n        // Finding out if the images are alone in their column.\n        let 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        const columnLeft = isImageWithoutPadding && !borderLeft ? imageEl.offsetLeft : columnEl.offsetLeft;\n        // Getting the width of the column.\n        const paddingLeft = parseFloat(style.paddingLeft);\n        let width = isImageWithoutPadding ? 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 = 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 ? parseFloat(imageEl.scrollHeight)\n            : parseFloat(columnEl.scrollHeight) - (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 = Math.round(columnTop / (rowSize + rowGap)) + 1 + (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-lg-' + 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 = /^(((pt|pb)\\d{1,3}$)|col-lg-|offset-lg-)/;\n        const toRemove = [...columnEl.classList].filter(c => {\n            return regex.test(c);\n        });\n        columnEl.classList.remove(...toRemove);\n        columnEl.classList.add('col-lg-' + 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 {Element} rowEl\n * @param {Element} columnEl\n * @param {Number} columnWidth the width in pixels of the column.\n * @param {Number} columnHeight the height in pixels of the column.\n * @returns {Object}\n */\nexport function _convertColumnToGrid(rowEl, columnEl, columnWidth, columnHeight) {\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 = parseFloat(rowEl.style.getPropertyValue(\"--grid-item-padding-x\")) || defaultGridPadding;\n    const paddingY = 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 gridProp = _getGridProperties(rowEl);\n    const columnColCount = Math.round((columnWidth + gridProp.columnGap) / (gridProp.columnSize + gridProp.columnGap));\n    const columnRowCount = Math.ceil((columnHeight + gridProp.rowGap) / (gridProp.rowSize + gridProp.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('g-col-lg-' + columnColCount, 'g-height-' + columnRowCount, 'col-lg-' + columnColCount);\n    columnEl.classList.add('o_grid_item');\n\n    return {columnColCount: columnColCount, columnRowCount: columnRowCount};\n}\n/**\n * Removes the grid properties from the grid column when it becomes a normal\n * column.\n *\n * @param {Element} columnEl\n */\nexport function _convertToNormalColumn(columnEl) {\n    const gridSizeClasses = columnEl.className.match(/(g-col-lg|g-height)-[0-9]+/g);\n    columnEl.classList.remove(\"o_grid_item\", \"o_grid_item_image\", \"o_grid_item_image_contain\", ...gridSizeClasses);\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(textNodeEl => textNodeEl.nodeValue.trim() === '');\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", "// 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 overflowY === 'auto' || overflowY === 'scroll' ||\n        (overflowY === 'visible' && element === element.ownerDocument.scrollingElement);\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 ? document.scrollingElement : (options.scrollable || closestScrollable(el.parentElement));\n    const scrollDocument = scrollable.ownerDocument;\n    const isInOneDocument = isTopOrBottomHidden || scrollDocument === el.ownerDocument;\n    const iframe = !isInOneDocument && Array.from(scrollable.querySelectorAll('iframe')).find(node => node.contentDocument.contains(el));\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 = offsetTop - (scrollable.getBoundingClientRect().top + window.scrollY - (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 = (scrollable === topLevelScrollable ? scrollFixedOffset(scrollDocument) : 0) + (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 = 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", "/** @odoo-module **/\n\nimport {SIZES, MEDIAS_BREAKPOINTS} from \"@web/core/ui/ui_service\";\nimport {\n    normalizeCSSColor,\n    isCSSColor,\n} from '@web/core/utils/colors';\n\nlet editableWindow = window;\nconst _setEditableWindow = (ew) => editableWindow = ew;\nlet editableDocument = document;\nconst _setEditableDocument = (ed) => editableDocument = ed;\n\nconst COLOR_PALETTE_COMPATIBILITY_COLOR_NAMES = ['primary', 'secondary', 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'success', 'info', 'warning', 'danger'];\n\n/**\n * These constants are colors that can be edited by the user when using\n * web_editor in a website context. We keep track of them so that color\n * palettes and their preview elements can always have the right colors\n * displayed even if website has redefined the colors during an editing\n * session.\n *\n * @type {string[]}\n */\nconst EDITOR_COLOR_CSS_VARIABLES = [...COLOR_PALETTE_COMPATIBILITY_COLOR_NAMES];\n// o-cc and o-colors\nfor (let i = 1; i <= 5; i++) {\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-color-${i}`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-bg`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-bg-gradient`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-headings`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-text`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-primary`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-primary-text`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-secondary`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-secondary-text`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-primary-border`);\n    EDITOR_COLOR_CSS_VARIABLES.push(`o-cc${i}-btn-secondary-border`);\n}\n// Grays\nfor (let i = 100; i <= 900; i += 100) {\n    EDITOR_COLOR_CSS_VARIABLES.push(`${i}`);\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 */\nconst CSS_SHORTHANDS = {\n    'border-width': ['border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'],\n    'border-radius': ['border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius'],\n    'border-color': ['border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color'],\n    'border-style': ['border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style'],\n    'padding': ['padding-top', 'padding-right', 'padding-bottom', 'padding-left'],\n};\n/**\n * Key-value mapping to list converters from an unit A to an unit B.\n * - The key is a string in the format '$1-$2' where $1 is the CSS symbol of\n *   unit A and $2 is the CSS symbol of unit B.\n * - The value is a function that converts the received value (expressed in\n *   unit A) to another value expressed in unit B. Two other parameters is\n *   received: the css property on which the unit applies and the jQuery element\n *   on which that css property may change.\n */\nconst CSS_UNITS_CONVERSION = {\n    's-ms': () => 1000,\n    'ms-s': () => 0.001,\n    'rem-px': () => _computePxByRem(),\n    'px-rem': () => _computePxByRem(true),\n    '%-px': () => -1, // Not implemented but should simply be ignored for now\n    'px-%': () => -1, // Not implemented but should simply be ignored for now\n};\n/**\n * Colors of the default palette, used for substitution in shapes/illustrations.\n * key: number of the color in the palette (ie, o-color-<1-5>)\n * value: color hex code\n */\nconst DEFAULT_PALETTE = {\n    '1': '#3AADAA',\n    '2': '#7C6576',\n    '3': '#F6F6F6',\n    '4': '#FFFFFF',\n    '5': '#383E45',\n};\n/**\n * Set of all the data attributes relative to the background images.\n */\nconst BACKGROUND_IMAGE_ATTRIBUTES = new Set([\n    \"originalId\", \"originalSrc\", \"mimetype\", \"resizeWidth\", \"glFilter\", \"quality\", \"bgSrc\",\n    \"filterOptions\",\n    \"mimetypeBeforeConversion\",\n]);\n\n/**\n * Computes the number of \"px\" needed to make a \"rem\" unit. Subsequent calls\n * returns the cached computed value.\n *\n * @param {boolean} [toRem=false]\n * @returns {float} - number of px by rem if 'toRem' is false\n *                  - the inverse otherwise\n */\nfunction _computePxByRem(toRem) {\n    if (editableDocument.PX_BY_REM === undefined) {\n        const htmlStyle = editableWindow.getComputedStyle(editableDocument.documentElement);\n        editableDocument.PX_BY_REM = parseFloat(htmlStyle['font-size']);\n    }\n    return toRem ? (1 / editableDocument.PX_BY_REM) : editableDocument.PX_BY_REM;\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 * @param {jQuery} [$target] - the jQuery element on which that css property\n *                             may change\n * @returns {number}\n */\nfunction _convertValueToUnit(value, unitTo, cssProp, $target) {\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, cssProp, $target);\n}\n/**\n * Converts the given numeric value expressed in the given css unit into\n * the corresponding numeric value expressed in the other given css unit.\n *\n * e.g. fct(400, 'ms', 's') -> 0.4\n *\n * @param {number} value\n * @param {string} unitFrom\n * @param {string} unitTo\n * @param {string} [cssProp] - the css property on which the unit applies\n * @param {jQuery} [$target] - the jQuery element on which that css property\n *                             may change\n * @returns {number}\n */\nfunction _convertNumericToUnit(value, unitFrom, unitTo, cssProp, $target) {\n    if (Math.abs(value) < Number.EPSILON || unitFrom === unitTo) {\n        return value;\n    }\n    const converter = CSS_UNITS_CONVERSION[`${unitFrom}-${unitTo}`];\n    if (converter === undefined) {\n        throw new Error(`Cannot convert '${unitFrom}' units into '${unitTo}' units !`);\n    }\n    return value * converter(cssProp, $target);\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 */\nfunction _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 {Node} [target] - the element on which that css property\n * @returns {boolean}\n */\nfunction _areCssValuesEqual(value1, value2, cssProp, target) {\n    const $target = $(target);\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    if (cssProp && cssProp.endsWith('-size')) {\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, $target)) {\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 (value1.startsWith('var(--')) {\n        value1 = _getCSSVariableValue(value1.substring(6, value1.length - 1));\n    }\n    if (value2.startsWith('var(--')) {\n        value2 = _getCSSVariableValue(value2.substring(6, value2.length - 1));\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], cssProp, $target);\n    return (Math.abs(numValue1 - numValue2) < Number.EPSILON);\n}\n/**\n * @param {string|number} name\n * @returns {boolean}\n */\nfunction _isColorCombinationName(name) {\n    const number = parseInt(name);\n    return (!isNaN(number) && number % 100 !== 0);\n}\n/**\n * @param {string[]} colorNames\n * @param {string} [prefix='bg-']\n * @returns {string[]}\n */\nfunction _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 * @param {string} key\n * @param {CSSStyleDeclaration} [htmlStyle] if not provided, it is computed\n * @returns {string}\n */\nfunction _getCSSVariableValue(key, htmlStyle) {\n    if (htmlStyle === undefined) {\n        htmlStyle = editableWindow.getComputedStyle(editableWindow.document.documentElement);\n    }\n    // Get trimmed value from the HTML element\n    let value = htmlStyle.getPropertyValue(`--${key}`).trim();\n    // If it is a color value, it needs to be normalized\n    value = normalizeCSSColor(value);\n    // Normally scss-string values are \"printed\" single-quoted. That way no\n    // magic conversation is needed when customizing a variable: either save it\n    // quoted for strings or non quoted for colors, numbers, etc. However,\n    // Chrome has the annoying behavior of changing the single-quotes to\n    // double-quotes when reading them through getPropertyValue...\n    return value.replace(/\"/g, \"'\");\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 */\nfunction _normalizeColor(color) {\n    if (isCSSColor(color)) {\n        return color;\n    }\n    return _getCSSVariableValue(color);\n}\n/**\n * Parse an element's background-image's url.\n *\n * @param {string} string a css value in the form 'url(\"...\")'\n * @returns {string|false} the src of the image or false if not parsable\n */\nfunction _getBgImageURL(el) {\n    const parts = _backgroundImageCssToParts($(el).css('background-image'));\n    const string = parts.url || '';\n    const match = string.match(/^url\\((['\"])(.*?)\\1\\)$/);\n    if (!match) {\n        return '';\n    }\n    const matchedURL = match[2];\n    // Make URL relative if possible\n    const fullURL = new URL(matchedURL, window.location.origin);\n    if (fullURL.origin === window.location.origin) {\n        return fullURL.href.slice(fullURL.origin.length);\n    }\n    return matchedURL;\n}\n/**\n * Extracts url and gradient parts from the background-image CSS property.\n *\n * @param {string} CSS 'background-image' property value\n * @returns {Object} contains the separated 'url' and 'gradient' parts\n */\nfunction _backgroundImageCssToParts(css) {\n    const parts = {};\n    css = css || '';\n    if (css.startsWith('url(')) {\n        const urlEnd = css.indexOf(')') + 1;\n        parts.url = css.substring(0, urlEnd).trim();\n        const commaPos = css.indexOf(',', urlEnd);\n        css = commaPos > 0 ? css.substring(commaPos + 1) : '';\n    }\n    if (_isColorGradient(css)) {\n        parts.gradient = css.trim();\n    }\n    return parts;\n}\n/**\n * Combines url and gradient parts into a background-image CSS property value\n *\n * @param {Object} contains the separated 'url' and 'gradient' parts\n * @returns {string} CSS 'background-image' property value\n */\nfunction _backgroundImagePartsToCss(parts) {\n    let css = parts.url || '';\n    if (parts.gradient) {\n        css += (css ? ', ' : '') + parts.gradient;\n    }\n    return css || 'none';\n}\n/**\n * @param {string} [value]\n * @returns {boolean}\n */\nfunction _isColorGradient(value) {\n    // FIXME duplicated in odoo-editor/utils.js\n    return value && value.includes('-gradient(');\n}\n/**\n * Generates a string ID.\n *\n * @private\n * @returns {string}\n */\nfunction _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 */\nfunction _getColorClass(el, colorNames, prefix) {\n    const prefixedColorNames = _computeColorClasses(colorNames, prefix);\n    return el.classList.value.split(' ').filter(cl => prefixedColorNames.includes(cl)).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 */\nfunction _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 */\nfunction _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 */\nfunction _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 the\n    // first non editable ancestor is in fact in an editable parent.\n    const parentEl = mediaEl.parentElement;\n    const nonEditableAncestorRootEl = parentEl && parentEl.closest('[contenteditable=\"false\"]');\n    return nonEditableAncestorRootEl\n        && nonEditableAncestorRootEl.parentElement\n        && nonEditableAncestorRootEl.parentElement.isContentEditable;\n}\n/**\n * Checks if the view of the targeted element is mobile.\n *\n * @param {HTMLElement} targetEl - target of the editor\n * @returns {boolean}\n */\nfunction _isMobileView(targetEl) {\n    const mobileViewThreshold = MEDIAS_BREAKPOINTS[SIZES.LG].minWidth;\n    const clientWidth = targetEl.ownerDocument.defaultView?.frameElement?.clientWidth ||\n        targetEl.ownerDocument.documentElement.clientWidth;\n    return clientWidth && clientWidth < mobileViewThreshold;\n}\n/**\n * Returns the label of a link element.\n *\n * @param {HTMLElement} linkEl\n * @returns {string}\n */\nfunction _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 */\nfunction _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(`.carousel-indicators [data-bs-slide-to=\"${imageIndex}\"]`);\n            if (miniatureEl && miniatureEl.style.backgroundImage) {\n                miniatureEl.style.backgroundImage = `url(${imgEl.getAttribute(\"src\")})`;\n            }\n        }\n    }\n}\n\nexport default {\n    COLOR_PALETTE_COMPATIBILITY_COLOR_NAMES: COLOR_PALETTE_COMPATIBILITY_COLOR_NAMES,\n    CSS_SHORTHANDS: CSS_SHORTHANDS,\n    CSS_UNITS_CONVERSION: CSS_UNITS_CONVERSION,\n    DEFAULT_PALETTE: DEFAULT_PALETTE,\n    EDITOR_COLOR_CSS_VARIABLES: EDITOR_COLOR_CSS_VARIABLES,\n    computePxByRem: _computePxByRem,\n    convertValueToUnit: _convertValueToUnit,\n    convertNumericToUnit: _convertNumericToUnit,\n    getNumericAndUnit: _getNumericAndUnit,\n    areCssValuesEqual: _areCssValuesEqual,\n    isColorCombinationName: _isColorCombinationName,\n    isColorGradient: _isColorGradient,\n    computeColorClasses: _computeColorClasses,\n    getCSSVariableValue: _getCSSVariableValue,\n    normalizeColor: _normalizeColor,\n    getBgImageURL: _getBgImageURL,\n    backgroundImageCssToParts: _backgroundImageCssToParts,\n    backgroundImagePartsToCss: _backgroundImagePartsToCss,\n    generateHTMLId: _generateHTMLId,\n    getColorClass: _getColorClass,\n    setEditableWindow: _setEditableWindow,\n    setEditableDocument: _setEditableDocument,\n    addBackgroundImageAttributes: _addBackgroundImageAttributes,\n    isBackgroundImageAttribute: _isBackgroundImageAttribute,\n    shouldEditableMediaBeEditable: _shouldEditableMediaBeEditable,\n    isMobileView: _isMobileView,\n    getLinkLabel: _getLinkLabel,\n    forwardToThumbnail: _forwardToThumbnail,\n};\n", "/** @odoo-module **/\n\nexport function isImg(node) {\n    return (node && (node.nodeName === \"IMG\" || (node.className && node.className.match(/(^|\\s)(media_iframe_video|o_image|fa)(\\s|$)/i))));\n}\n\n/**\n * Returns a list of all the ancestors nodes of the provided node.\n *\n * @param {Node} node\n * @param {Node} [stopElement] include to prevent bubbling up further than the stopElement.\n * @returns {HTMLElement[]}\n */\nexport function ancestors(node, stopElement) {\n    if (!node || !node.parentElement || node === stopElement) return [];\n    return [node.parentElement, ...ancestors(node.parentElement, stopElement)];\n}", "import { App, Component, useState, xml } from \"@odoo/owl\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst rootTemplate = xml`<SubComp t-props=\"state\"/>`;\nexport async function attachComponent(parent, element, componentClass, props = {}) {\n    class Root extends Component {\n        static template = rootTemplate;\n        static components = { SubComp: componentClass };\n        static props = [\"*\"];\n        state = useState(props);\n    }\n\n    const env = Component.env;\n    const app = new App(Root, {\n        env,\n        getTemplate,\n        dev: env.debug,\n        translatableAttributes: [\"data-tooltip\"],\n        translateFn: _t,\n    });\n\n    if (parent.__parentedMixin) {\n        parent.__parentedChildren.push({\n            get $el() {\n                return $(app.root.el);\n            },\n            destroy() {\n                app.destroy();\n            },\n        });\n    }\n\n    const originalValidateTarget = App.validateTarget;\n    App.validateTarget = () => {};\n    const mountPromise = app.mount(element);\n    App.validateTarget = originalValidateTarget;\n    const component = await mountPromise;\n    const subComp = Object.values(component.__owl__.children)[0].component;\n    return {\n        component: subComp,\n        destroy() {\n            app.destroy();\n        },\n        update(props) {\n            Object.assign(component.state, props);\n        },\n    };\n}\n", "/** @odoo-module **/\n\nexport const DIRECTIONS = {\n    LEFT: false,\n    RIGHT: true,\n};\nexport const CTYPES = {\n    // Short for CONTENT_TYPES\n    // Inline group\n    CONTENT: 1,\n    SPACE: 2,\n\n    // Block group\n    BLOCK_OUTSIDE: 4,\n    BLOCK_INSIDE: 8,\n\n    // Br group\n    BR: 16,\n};\nexport function ctypeToString(ctype) {\n    return Object.keys(CTYPES).find((key) => CTYPES[key] === ctype);\n}\nexport const CTGROUPS = {\n    // Short for CONTENT_TYPE_GROUPS\n    INLINE: CTYPES.CONTENT | CTYPES.SPACE,\n    BLOCK: CTYPES.BLOCK_OUTSIDE | CTYPES.BLOCK_INSIDE,\n    BR: CTYPES.BR,\n};\nconst tldWhitelist = [\n    'com', 'net', 'org', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an',\n    'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd',\n    'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bl', 'bm', 'bn', 'bo', 'br', 'bq',\n    'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch',\n    'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cs', 'cu', 'cv', 'cw', 'cx',\n    'cy', 'cz', 'dd', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg',\n    'eh', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga',\n    'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', 'gq',\n    'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu',\n    'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm',\n    'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky',\n    'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly',\n    'ma', 'mc', 'md', 'me', 'mf', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo',\n    'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na',\n    'nc', 'ne', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om',\n    'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt',\n    'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 'sc', 'sd',\n    'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', 'ss',\n    'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj',\n    'tk', 'tl', 'tm', 'tn', 'to', 'tp', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua',\n    'ug', 'uk', 'um', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn',\n    'vu', 'wf', 'ws', 'ye', 'yt', 'yu', 'za', 'zm', 'zr', 'zw', 'co\\\\.uk'];\n\nconst urlRegexBase = `|(?:www.))[-a-zA-Z0-9@:%._\\\\+~#=]{2,256}\\\\.[a-zA-Z][a-zA-Z0-9]{1,62}|(?:[-a-zA-Z0-9@:%._\\\\+~#=]{2,256}\\\\.(?:${tldWhitelist.join('|')})\\\\b))(?:(?:[/?#])[^\\\\s]*[^!.,})\\\\]'\"\\\\s]|(?:[^!(){}.,[\\\\]'\"\\\\s]+))?`;\nconst httpCapturedRegex= `(https?:\\\\/\\\\/)`;\n\nexport const URL_REGEX = new RegExp(`((?:(?:${httpCapturedRegex}${urlRegexBase})`, 'i');\nexport const YOUTUBE_URL_GET_VIDEO_ID =\n    /^(?:(?:https?:)?\\/\\/)?(?:(?:www|m)\\.)?(?:youtube\\.com|youtu\\.be)(?:\\/(?:[\\w-]+\\?v=|embed\\/|v\\/)?)([^\\s?&#]+)(?:\\S+)?$/i;\nexport const EMAIL_REGEX = /^(mailto:)?[\\w-.]+@(?:[\\w-]+\\.)+[\\w-]{2,4}$/i;\nexport const PHONE_REGEX = /^(tel:(?:\\/\\/)?)?\\+?[\\d\\s.\\-()\\/]{3,25}$/;\n\nexport const PROTECTED_BLOCK_TAG = ['TR','TD','TABLE','TBODY','UL','OL','LI'];\n\n/**\n * Array of all the classes used by the editor to change the font size.\n *\n * Note: the Bootstrap \"small\" class is an exception, the editor does not allow\n * to set it but it did in the past and we want to remove it when applying an\n * override of the font-size.\n */\nexport const FONT_SIZE_CLASSES = [\"display-1-fs\", \"display-2-fs\", \"display-3-fs\", \"display-4-fs\", \"h1-fs\",\n    \"h2-fs\", \"h3-fs\", \"h4-fs\", \"h5-fs\", \"h6-fs\", \"base-fs\", \"o_small-fs\", \"small\"];\n\n/**\n * Array of all the classes used by the editor to change the text style.\n *\n * Note: the Bootstrap \"small\" class was actually part of \"text style\"\n * configuration in the past... but also of the \"font size\" configuration (see\n * FONT_SIZE_CLASSES). It should be mentioned here too.\n */\nexport const TEXT_STYLE_CLASSES = [\"display-1\", \"display-2\", \"display-3\", \"display-4\", \"lead\", \"o_small\", \"small\"];\n\nconst ZWNBSP_CHAR = '\\ufeff';\nexport const ZERO_WIDTH_CHARS = ['\\u200b', ZWNBSP_CHAR];\nexport const ZERO_WIDTH_CHARS_REGEX = new RegExp(`[${ZERO_WIDTH_CHARS.join('')}]`, 'g');\n\n//------------------------------------------------------------------------------\n// Position and sizes\n//------------------------------------------------------------------------------\n\n/**\n * @param {Node} node\n * @returns {Array.<HTMLElement, number>}\n */\nexport function leftPos(node) {\n    return [node.parentNode, childNodeIndex(node)];\n}\n/**\n * @param {Node} node\n * @returns {Array.<HTMLElement, number>}\n */\nexport function rightPos(node) {\n    return [node.parentNode, childNodeIndex(node) + 1];\n}\n/**\n * @param {Node} node\n * @returns {Array.<HTMLElement, number, HTMLElement, number>}\n */\nexport function boundariesOut(node) {\n    const index = childNodeIndex(node);\n    return [node.parentNode, index, node.parentNode, index + 1];\n}\n/**\n * @param {Node} node\n * @returns {Array.<Node, number>}\n */\nexport function startPos(node) {\n    return [node, 0];\n}\n/**\n * @param {Node} node\n * @returns {Array.<Node, number>}\n */\nexport function endPos(node) {\n    return [node, nodeSize(node)];\n}\n/**\n * @param {Node} node\n * @returns {Array.<node, number, node, number>}\n */\nexport function boundariesIn(node) {\n    return [node, 0, node, nodeSize(node)];\n}\n/**\n * Returns the given node's position relative to its parent (= its index in the\n * child nodes of its parent).\n *\n * @param {Node} node\n * @returns {number}\n */\nexport function childNodeIndex(node) {\n    let i = 0;\n    while (node.previousSibling) {\n        i++;\n        node = node.previousSibling;\n    }\n    return i;\n}\n/**\n * Returns the size of the node = the number of characters for text nodes and\n * the number of child nodes for element nodes.\n *\n * @param {Node} node\n * @returns {number}\n */\nexport function nodeSize(node) {\n    const isTextNode = node.nodeType === Node.TEXT_NODE;\n    return isTextNode ? node.length : node.childNodes.length;\n}\n\n//------------------------------------------------------------------------------\n// DOM Path and node search functions\n//------------------------------------------------------------------------------\n\nexport const closestPath = function* (node) {\n    while (node) {\n        yield node;\n        node = node.parentNode;\n    }\n};\n\n/**\n * Values which can be returned while browsing the DOM which gives information\n * to why the path ended.\n */\nconst PATH_END_REASONS = {\n    NO_NODE: 0,\n    BLOCK_OUT: 1,\n    BLOCK_HIT: 2,\n    OUT_OF_SCOPE: 3,\n};\n/**\n * Creates a generator function according to the given parameters. Pre-made\n * generators to traverse the DOM are made using this function:\n *\n * @see leftLeafFirstPath\n * @see leftLeafOnlyNotBlockPath\n * @see leftLeafOnlyInScopeNotBlockEditablePath\n * @see rightLeafOnlyNotBlockPath\n * @see rightLeafOnlyPathNotBlockNotEditablePath\n * @see rightLeafOnlyInScopeNotBlockEditablePath\n * @see rightLeafOnlyNotBlockNotEditablePath\n *\n * @param {number} direction\n * @param {boolean} [options.leafOnly] if true, do not yield any non-leaf node\n * @param {boolean} [options.inScope] if true, stop the generator as soon as a node is not\n *                      a descendant of `node` provided when traversing the\n *                      generated function.\n * @param {Function} [options.stopTraverseFunction] a function that takes a node\n *                      and should return true when a node descendant should not\n *                      be traversed.\n * @param {Function} [options.stopFunction] function that makes the generator stop when a\n *                      node is encountered.\n */\nexport function createDOMPathGenerator(\n    direction,\n    { leafOnly = false, inScope = false, stopTraverseFunction, stopFunction } = {},\n) {\n    const nextDeepest =\n        direction === DIRECTIONS.LEFT\n            ? node => lastLeaf(node.previousSibling, stopTraverseFunction)\n            : node => firstLeaf(node.nextSibling, stopTraverseFunction);\n\n    const firstNode =\n        direction === DIRECTIONS.LEFT\n            ? (node, offset) => lastLeaf(node.childNodes[offset - 1], stopTraverseFunction)\n            : (node, offset) => firstLeaf(node.childNodes[offset], stopTraverseFunction);\n\n    // Note \"reasons\" is a way for the caller to be able to know why the\n    // generator ended yielding values.\n    return function* (node, offset, reasons = []) {\n        let movedUp = false;\n\n        let currentNode = firstNode(node, offset);\n        if (!currentNode) {\n            movedUp = true;\n            currentNode = node;\n        }\n\n        while (currentNode) {\n            if (stopFunction && stopFunction(currentNode)) {\n                reasons.push(movedUp ? PATH_END_REASONS.BLOCK_OUT : PATH_END_REASONS.BLOCK_HIT);\n                break;\n            }\n            if (inScope && currentNode === node) {\n                reasons.push(PATH_END_REASONS.OUT_OF_SCOPE);\n                break;\n            }\n            if (!(leafOnly && movedUp)) {\n                yield currentNode;\n            }\n\n            movedUp = false;\n            let nextNode = nextDeepest(currentNode);\n            if (!nextNode) {\n                movedUp = true;\n                nextNode = currentNode.parentNode;\n            }\n            currentNode = nextNode;\n        }\n\n        reasons.push(PATH_END_REASONS.NO_NODE);\n    };\n}\n\n/**\n * Find a node.\n * @param {findCallback} findCallback - This callback check if this function\n *      should return `node`.\n * @param {findCallback} stopCallback - This callback check if this function\n *      should stop when it receive `node`.\n */\nexport function findNode(domPath, findCallback = () => true, stopCallback = () => false) {\n    for (const node of domPath) {\n        if (findCallback(node)) {\n            return node;\n        }\n        if (stopCallback(node)) {\n            break;\n        }\n    }\n    return null;\n}\n/**\n * This callback check if findNode should return `node`.\n * @callback findCallback\n * @param {Node} node\n * @return {Boolean}\n */\n/**\n * This callback check if findNode should stop when it receive `node`.\n * @callback stopCallback\n * @param {Node} node\n */\n\n/**\n * Return the furthest uneditable parent of node contained within parentLimit.\n * @see deleteRange Used to guarantee that uneditables are fully contained in\n * the range (so that it is not possible to partially remove them)\n *\n * @param {Node} node\n * @param {Node} [parentLimit=undefined] non-inclusive furthest parent allowed\n * @returns {Node} uneditable parent if it exists\n */\nexport function getFurthestUneditableParent(node, parentLimit) {\n    if (node === parentLimit || (parentLimit && !parentLimit.contains(node))) {\n        return undefined;\n    }\n    let parent = node && node.parentElement;\n    let nonEditableElement;\n    while (parent && (!parentLimit || parent !== parentLimit)) {\n        if (!parent.isContentEditable) {\n            nonEditableElement = parent;\n        }\n        if (parent.oid === \"root\") {\n            break;\n        }\n        parent = parent.parentElement;\n    }\n    return nonEditableElement;\n}\n/**\n * Returns the closest HTMLElement of the provided Node. If the predicate is a\n * string, returns the closest HTMLElement that match the predicate selector. If\n * the predicate is a function, returns the closest element that matches the\n * predicate. Any returned element will be contained within the editable.\n *\n * @param {Node} node\n * @param {string | Function} [predicate='*']\n * @returns {HTMLElement|null}\n */\nexport function closestElement(node, predicate = \"*\") {\n    if (!node) return null;\n    let element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;\n    if (typeof predicate === 'function') {\n        while (element && !predicate(element)) {\n            element = element.parentElement;\n        }\n    } else {\n        element = element?.closest(predicate);\n    }\n\n    return element?.closest('.odoo-editor-editable') && element;\n}\n\n/**\n * Returns a list of all the ancestors nodes of the provided node.\n *\n * @param {Node} node\n * @param {Node} [editable] include to prevent bubbling up further than the editable.\n * @returns {HTMLElement[]}\n */\nexport function ancestors(node, editable) {\n    if (!node || !node.parentElement || node === editable) return [];\n    return [node.parentElement, ...ancestors(node.parentElement, editable)];\n}\n\n/**\n * Take a node, return all of its descendants, in depth-first order.\n *\n * @param {Node} node\n * @returns {Node[]}\n */\nexport function descendants(node) {\n    const posterity = [];\n    for (const child of (node.childNodes || [])) {\n        posterity.push(child, ...descendants(child));\n    }\n    return posterity;\n}\n\nexport function closestBlock(node) {\n    return findNode(closestPath(node), node => isBlock(node));\n}\n/**\n * Returns the deepest child in last position.\n *\n * @param {Node} node\n * @param {Function} [stopTraverseFunction]\n * @returns {Node}\n */\nexport function lastLeaf(node, stopTraverseFunction) {\n    while (node && node.lastChild && !(stopTraverseFunction && stopTraverseFunction(node))) {\n        node = node.lastChild;\n    }\n    return node;\n}\n/**\n * Returns the deepest child in first position.\n *\n * @param {Node} node\n * @param {Function} [stopTraverseFunction]\n * @returns {Node}\n */\nexport function firstLeaf(node, stopTraverseFunction) {\n    while (node && node.firstChild && !(stopTraverseFunction && stopTraverseFunction(node))) {\n        node = node.firstChild;\n    }\n    return node;\n}\nexport function previousLeaf(node, editable, skipInvisible = false) {\n    let ancestor = node;\n    while (ancestor && !ancestor.previousSibling && ancestor !== editable) {\n        ancestor = ancestor.parentElement;\n    }\n    if (ancestor && ancestor !== editable) {\n        if (skipInvisible && !isVisible(ancestor.previousSibling)) {\n            return previousLeaf(ancestor.previousSibling, editable, skipInvisible);\n        } else {\n            const last = lastLeaf(ancestor.previousSibling);\n            if (skipInvisible && !isVisible(last)) {\n                return previousLeaf(last, editable, skipInvisible);\n            } else {\n                return last;\n            }\n        }\n    }\n}\nexport function nextLeaf(node, editable, skipInvisible = false) {\n    let ancestor = node;\n    while (ancestor && !ancestor.nextSibling && ancestor !== editable) {\n        ancestor = ancestor.parentElement;\n    }\n    if (ancestor && ancestor !== editable) {\n        if (skipInvisible && ancestor.nextSibling && !isVisible(ancestor.nextSibling)) {\n            return nextLeaf(ancestor.nextSibling, editable, skipInvisible);\n        } else {\n            const first = firstLeaf(ancestor.nextSibling);\n            if (skipInvisible && !isVisible(first)) {\n                return nextLeaf(first, editable, skipInvisible);\n            } else {\n                return first;\n            }\n        }\n    }\n}\n/**\n * Returns all the previous siblings of the given node until the first\n * sibling that does not satisfy the predicate, in lookup order.\n *\n * @param {Node} node\n * @param {Function} [predicate] (node: Node) => boolean\n */\nexport function getAdjacentPreviousSiblings(node, predicate = n => !!n) {\n    let previous = node.previousSibling;\n    const list = [];\n    while (previous && predicate(previous)) {\n        list.push(previous);\n        previous = previous.previousSibling;\n    }\n    return list;\n}\n/**\n * Returns all the next siblings of the given node until the first\n * sibling that does not satisfy the predicate, in lookup order.\n *\n * @param {Node} node\n * @param {Function} [predicate] (node: Node) => boolean\n */\nexport function getAdjacentNextSiblings(node, predicate = n => !!n) {\n    let next = node.nextSibling;\n    const list = [];\n    while (next && predicate(next)) {\n        list.push(next);\n        next = next.nextSibling;\n    }\n    return list;\n}\n/**\n * Returns all the adjacent siblings of the given node until the first sibling\n * (in both directions) that does not satisfy the predicate, in index order. If\n * the given node does not satisfy the predicate, an empty array is returned.\n *\n * @param {Node} node\n * @param {Function} [predicate] (node: Node) => boolean\n */\nexport function getAdjacents(node, predicate = n => !!n) {\n    const previous = getAdjacentPreviousSiblings(node, predicate);\n    const next = getAdjacentNextSiblings(node, predicate);\n    return predicate(node) ? [...previous.reverse(), node, ...next] : [];\n}\n\n//------------------------------------------------------------------------------\n// Cursor management\n//------------------------------------------------------------------------------\n\n/**\n * Returns true if the given editable area contains a table with selected cells.\n *\n * @param {Element} editable\n * @returns {boolean}\n */\nexport function hasTableSelection(editable) {\n    return !!editable.querySelector('.o_selected_table');\n}\n/**\n * Returns true if the given editable area contains a \"valid\" selection, by\n * which we mean a browser selection whose elements are defined, or a table with\n * selected cells.\n *\n * @param {Element} editable\n * @returns {boolean}\n */\nexport function hasValidSelection(editable) {\n    return hasTableSelection(editable) || editable.ownerDocument.getSelection().rangeCount > 0;\n}\n/**\n * From a given position, returns the normalized version.\n *\n * E.g. <b>abc</b>[]def -> <b>abc[]</b>def\n *\n * @param {Node} node\n * @param {number} offset\n * @param {boolean} [full=true] (if not full, it means we only normalize\n *     positions which are not possible, like the cursor inside an image).\n */\nexport function getNormalizedCursorPosition(node, offset, full = true) {\n    const editable = closestElement(node, '.odoo-editor-editable');\n    let closest = closestElement(node);\n    while (\n        closest &&\n        closest !== editable &&\n        (isSelfClosingElement(node) || !closest.isContentEditable)\n    ) {\n        // Cannot put the cursor inside those elements, put it before if the\n        // offset is 0 and the node is not empty, else after instead.\n        [node, offset] = offset || !nodeSize(node) ? rightPos(node) : leftPos(node);\n        closest = closestElement(node);\n    }\n\n    // Be permissive about the received offset.\n    offset = Math.min(Math.max(offset, 0), nodeSize(node));\n\n    if (full) {\n        // Put the cursor in deepest inline node around the given position if\n        // possible.\n        let el;\n        let elOffset;\n        if (node.nodeType === Node.ELEMENT_NODE) {\n            el = node;\n            elOffset = offset;\n        } else if (node.nodeType === Node.TEXT_NODE) {\n            if (offset === 0) {\n                el = node.parentNode;\n                elOffset = childNodeIndex(node);\n            } else if (offset === node.length) {\n                el = node.parentNode;\n                elOffset = childNodeIndex(node) + 1;\n            }\n        }\n        if (el) {\n            const leftInlineNode = leftLeafOnlyInScopeNotBlockEditablePath(el, elOffset).next().value;\n            let leftVisibleEmpty = false;\n            if (leftInlineNode) {\n                leftVisibleEmpty =\n                    isSelfClosingElement(leftInlineNode) ||\n                    !closestElement(leftInlineNode).isContentEditable;\n                [node, offset] = leftVisibleEmpty\n                    ? rightPos(leftInlineNode)\n                    : endPos(leftInlineNode);\n            }\n            if (!leftInlineNode || leftVisibleEmpty) {\n                const rightInlineNode = rightLeafOnlyInScopeNotBlockEditablePath(el, elOffset).next().value;\n                if (rightInlineNode) {\n                    const closest = closestElement(rightInlineNode);\n                    const rightVisibleEmpty =\n                        isSelfClosingElement(rightInlineNode) ||\n                        !closest ||\n                        !closest.isContentEditable;\n                    if (!(leftVisibleEmpty && rightVisibleEmpty)) {\n                        [node, offset] = rightVisibleEmpty\n                            ? leftPos(rightInlineNode)\n                            : startPos(rightInlineNode);\n                    }\n                }\n            }\n        }\n    }\n\n    const prevNode = node.nodeType === Node.ELEMENT_NODE && node.childNodes[offset - 1];\n    if (prevNode && prevNode.nodeName === 'BR' && isFakeLineBreak(prevNode)) {\n        // If trying to put the cursor on the right of a fake line break, put\n        // it before instead.\n        offset--;\n    }\n\n    return [node, offset];\n}\nexport function insertSelectionChars(anchorNode, anchorOffset, focusNode, focusOffset, startChar='[', endChar=']') {\n    // If the range characters have to be inserted within the same parent and\n    // the anchor range character has to be before the focus range character,\n    // the focus offset needs to be adapted to account for the first insertion.\n    if (anchorNode === focusNode && anchorOffset <= focusOffset) {\n        focusOffset += (focusNode.nodeType === Node.TEXT_NODE ? startChar.length : 1);\n    }\n    insertCharsAt(startChar, anchorNode, anchorOffset);\n    insertCharsAt(endChar, focusNode, focusOffset);\n}\n/**\n * Log the contents of the given root, with the characters \"[\" and \"]\" around\n * the selection.\n *\n * @param {Element} root\n * @param {Object} [options={}]\n * @param {Selection} [options.selection] if undefined, the current selection is used.\n * @param {boolean} [options.doFormat] if true, the HTML is formatted.\n * @param {boolean} [options.includeOids] if true, the HTML is formatted.\n */\nexport function logSelection(root, options = {}) {\n    const sel = options.selection || root.ownerDocument.getSelection();\n    if (!root.contains(sel.anchorNode) || !root.contains(sel.focusNode)) {\n        console.warn('The selection is not contained in the root.');\n        return;\n    }\n\n    // Clone the root and its contents.\n    let anchorClone, focusClone;\n    const cloneTree = node => {\n        const clone = node.cloneNode();\n        if (options.includeOids) {\n            clone.oid = node.oid;\n        }\n        anchorClone = anchorClone || (node === sel.anchorNode && clone);\n        focusClone = focusClone || (node === sel.focusNode && clone);\n        for (const child of node.childNodes || []) {\n            clone.append(cloneTree(child));\n        }\n        return clone;\n    }\n    const rootClone = cloneTree(root);\n\n    // Insert the selection characters.\n    insertSelectionChars(anchorClone, sel.anchorOffset, focusClone, sel.focusOffset, '%c[%c', '%c]%c');\n\n    // Remove information that is not useful for the log.\n    rootClone.removeAttribute('data-last-history-steps');\n\n    // Format the HTML by splitting and indenting to highlight the structure.\n    if (options.doFormat) {\n        const formatHtml = (node, spaces = 0) => {\n            node.before(document.createTextNode('\\n' + ' '.repeat(spaces)));\n            for (const child of [...node.childNodes]) {\n                formatHtml(child, spaces + 4);\n            }\n            if (node.nodeType !== Node.TEXT_NODE) {\n                node.appendChild(document.createTextNode('\\n' + ' '.repeat(spaces)));\n            }\n            if (options.includeOids) {\n                if (node.nodeType === Node.TEXT_NODE) {\n                    node.textContent += ` (${node.oid})`;\n                } else {\n                    node.setAttribute('oid', node.oid);\n                }\n            }\n        }\n        formatHtml(rootClone);\n    }\n\n    // Style and log the result.\n    const selectionCharacterStyle = 'color: #75bfff; font-weight: 700;';\n    const defaultStyle = 'color: inherit; font-weight: inherit;';\n    console.log(\n        makeZeroWidthCharactersVisible(rootClone.outerHTML),\n        selectionCharacterStyle, defaultStyle, selectionCharacterStyle, defaultStyle,\n    );\n}\n/**\n * Guarantee that the focus is on element or one of its children.\n *\n * A simple call to element.focus will change the editable context\n * if one of the parents of the current activeElement is not editable,\n * and the caret position will not be preserved, even if activeElement is\n * one of the subchildren of element. This is why the (re)focus is\n * only called when the current activeElement is not one of the\n * (sub)children of element.\n *\n * @param {Element} element should have the focus or a child with the focus\n */\n export function ensureFocus(element) {\n    const activeElement = element.ownerDocument.activeElement;\n    if (activeElement !== element && (!element.contains(activeElement) || !activeElement.isContentEditable)) {\n        element.focus();\n    }\n}\n/**\n * @param {Node} anchorNode\n * @param {number} anchorOffset\n * @param {Node} focusNode\n * @param {number} focusOffset\n * @param {boolean} [normalize=true]\n * @returns {?Array.<Node, number}\n */\nexport function setSelection(\n    anchorNode,\n    anchorOffset,\n    focusNode = anchorNode,\n    focusOffset = anchorOffset,\n    normalize = true,\n) {\n    if (\n        !anchorNode ||\n        !anchorNode.parentElement ||\n        !anchorNode.parentElement.closest('body') ||\n        !focusNode ||\n        !focusNode.parentElement ||\n        !focusNode.parentElement.closest('body')\n    ) {\n        return null;\n    }\n    const document = anchorNode.ownerDocument;\n\n    const seemsCollapsed = anchorNode === focusNode && anchorOffset === focusOffset;\n    [anchorNode, anchorOffset] = getNormalizedCursorPosition(anchorNode, anchorOffset, normalize);\n    [focusNode, focusOffset] = seemsCollapsed\n        ? [anchorNode, anchorOffset]\n        : getNormalizedCursorPosition(focusNode, focusOffset, normalize);\n\n    const direction = getCursorDirection(anchorNode, anchorOffset, focusNode, focusOffset);\n    const sel = document.getSelection();\n    if (!sel) {\n        return null;\n    }\n    try {\n        const range = new Range();\n        if (direction === DIRECTIONS.RIGHT) {\n            range.setStart(anchorNode, anchorOffset);\n            range.collapse(true);\n        } else {\n            range.setEnd(anchorNode, anchorOffset);\n            range.collapse(false);\n        }\n        sel.removeAllRanges();\n        sel.addRange(range);\n        sel.extend(focusNode, focusOffset);\n    } catch (e) {\n        // Firefox throws NS_ERROR_FAILURE when setting selection on element\n        // with contentEditable=false for no valid reason since non-editable\n        // content are selectable by the user anyway.\n        if (e.name !== 'NS_ERROR_FAILURE') {\n            throw e;\n        }\n    }\n\n    return [anchorNode, anchorOffset, focusNode, focusOffset];\n}\n/**\n * @param {Node} node\n * @param {boolean} [normalize=true]\n * @returns {?Array.<Node, number}\n */\nexport function setCursorStart(node, normalize = true) {\n    const pos = startPos(node);\n    return setSelection(...pos, ...pos, normalize);\n}\n/**\n * @param {Node} node\n * @param {boolean} [normalize=true]\n * @returns {?Array.<Node, number}\n */\nexport function setCursorEnd(node, normalize = true) {\n    const pos = endPos(node);\n    return setSelection(...pos, ...pos, normalize);\n}\n/**\n * From selection position, checks if it is left-to-right or right-to-left.\n *\n * @param {Node} anchorNode\n * @param {number} anchorOffset\n * @param {Node} focusNode\n * @param {number} focusOffset\n * @returns {boolean} the direction of the current range if the selection not is collapsed | false\n */\nexport function getCursorDirection(anchorNode, anchorOffset, focusNode, focusOffset) {\n    if (anchorNode === focusNode) {\n        if (anchorOffset === focusOffset) return false;\n        return anchorOffset < focusOffset ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n    }\n    return anchorNode.compareDocumentPosition(focusNode) & Node.DOCUMENT_POSITION_FOLLOWING\n        ? DIRECTIONS.RIGHT\n        : DIRECTIONS.LEFT;\n}\n/**\n * Returns an array containing all the nodes traversed when walking the\n * selection.\n *\n * @param {Node} editable\n * @returns {Node[]}\n */\nexport function getTraversedNodes(editable, range = getDeepRange(editable)) {\n    const selectedTableCells = editable.querySelectorAll('.o_selected_td');\n    const document = editable.ownerDocument;\n    if (!range) return [];\n    const iterator = document.createNodeIterator(range.commonAncestorContainer);\n    let node;\n    do {\n        node = iterator.nextNode();\n    } while (node && node !== range.startContainer && !(selectedTableCells.length && node === selectedTableCells[0]));\n    if (\n        node &&\n        !(selectedTableCells.length && node === selectedTableCells[0]) &&\n        !range.collapsed &&\n        node.nodeType === Node.ELEMENT_NODE &&\n        node.childNodes.length &&\n        range.startOffset &&\n        node.childNodes[range.startOffset - 1].nodeName === \"BR\"\n    ) {\n        // Handle the cases:\n        // <p>ab<br>[</p><p>cd</p>] => [p2, cd]\n        // <p>ab<br>[<br>cd</p><p>ef</p>] => [br2, cd, p2, ef]\n        const targetBr = node.childNodes[range.startOffset - 1];\n        while (node != targetBr) {\n            node = iterator.nextNode();\n        }\n        node = iterator.nextNode();\n    }\n    if (\n        node &&\n        !range.collapsed &&\n        node === range.startContainer &&\n        range.startOffset === nodeSize(node) &&\n        node.nextSibling &&\n        node.nextSibling.nodeName === \"BR\"\n    ) {\n        // Handle the case: <p>ab[<br>cd</p><p>ef</p>] => [br, cd, p2, ef]\n        node = iterator.nextNode();\n    }\n    const traversedNodes = new Set([node, ...descendants(node)]);\n    while (node && node !== range.endContainer) {\n        node = iterator.nextNode();\n        if (node) {\n            const selectedTable = closestElement(node, '.o_selected_table');\n            if (selectedTable) {\n                for (const selectedTd of selectedTable.querySelectorAll('.o_selected_td')) {\n                    traversedNodes.add(selectedTd);\n                    descendants(selectedTd).forEach(descendant => traversedNodes.add(descendant));\n                }\n            } else if (\n                !(\n                    // Handle the case: [<p>ab</p><p>cd<br>]ef</p> => [ab, p2, cd, br]\n                    node === range.endContainer &&\n                    range.endOffset === 0 &&\n                    !range.collapsed &&\n                    node.previousSibling &&\n                    node.previousSibling.nodeName === \"BR\"\n                )\n            ) {\n                traversedNodes.add(node);\n            }\n        }\n    }\n    if (node) {\n        // Handle the cases:\n        // [<p>ab</p><p>cd<br>]</p> => [ab, p2, cd, br]\n        // [<p>ab</p><p>cd<br>]<br>ef</p> => [ab, p2, cd, br1]\n        for (const descendant of descendants(node)) {\n            if (\n                descendant.parentElement === node &&\n                childNodeIndex(descendant) >= range.endOffset\n            ) {\n                break;\n            }\n            traversedNodes.add(descendant);\n        }\n    }\n    return [...traversedNodes];\n}\n/**\n * Returns an array containing all the nodes fully contained in the selection.\n *\n * @param {Node} editable\n * @returns {Node[]}\n */\nexport function getSelectedNodes(editable) {\n    const selectedTableCells = editable.querySelectorAll('.o_selected_td');\n    const document = editable.ownerDocument;\n    const sel = document.getSelection();\n    if (!sel.rangeCount && !selectedTableCells.length) {\n        return [];\n    }\n    const range = sel.getRangeAt(0);\n    return [...new Set(getTraversedNodes(editable).flatMap(\n        node => {\n            const td = closestElement(node, '.o_selected_td');\n            if (td) {\n                return descendants(td);\n            } else if (range.isPointInRange(node, 0) && range.isPointInRange(node, nodeSize(node))) {\n                return node;\n            } else {\n                return [];\n            }\n        },\n    ))];\n}\n\n/**\n * Returns the current range (if any), adapted to target the deepest\n * descendants.\n *\n * @param {Node} editable\n * @param {object} [options]\n * @param {Selection} [options.range] the range to use.\n * @param {Selection} [options.sel] the selection to use.\n * @param {boolean} [options.splitText] split the targeted text nodes at offset.\n * @param {boolean} [options.select] select the new range if it changed (via splitText).\n * @param {boolean} [options.correctTripleClick] adapt the range if it was a triple click.\n * @returns {Range}\n */\nexport function getDeepRange(editable, { range, sel, splitText, select, correctTripleClick } = {}) {\n    sel = sel || editable.parentElement && editable.ownerDocument.getSelection();\n    if (sel && sel.isCollapsed && sel.anchorNode && sel.anchorNode.nodeName === \"BR\") {\n        setSelection(sel.anchorNode.parentElement, childNodeIndex(sel.anchorNode));\n    }\n    range = range ? range.cloneRange() : sel && sel.rangeCount && sel.getRangeAt(0).cloneRange();\n    if (!range) return;\n    let start = range.startContainer;\n    let startOffset = range.startOffset;\n    let end = range.endContainer;\n    let endOffset = range.endOffset;\n\n    const isBackwards =\n        !range.collapsed && start === sel.focusNode && startOffset === sel.focusOffset;\n\n    // Target the deepest descendant of the range nodes.\n    [start, startOffset] = getDeepestPosition(start, startOffset);\n    [end, endOffset] = getDeepestPosition(end, endOffset);\n\n    // Split text nodes if that was requested.\n    if (splitText) {\n        const isInSingleContainer = start === end;\n        if (\n            end.nodeType === Node.TEXT_NODE &&\n            endOffset !== 0 &&\n            endOffset !== end.textContent.length\n        ) {\n            const endParent = end.parentNode;\n            const splitOffset = splitTextNode(end, endOffset);\n            end = endParent.childNodes[splitOffset - 1] || endParent.firstChild;\n            if (isInSingleContainer) {\n                start = end;\n            }\n            endOffset = end.textContent.length;\n        }\n        if (\n            start.nodeType === Node.TEXT_NODE &&\n            startOffset !== 0 &&\n            startOffset !== start.textContent.length\n        ) {\n            splitTextNode(start, startOffset);\n            startOffset = 0;\n            if (isInSingleContainer) {\n                endOffset = start.textContent.length;\n            }\n        }\n    }\n    // A selection spanning multiple nodes and ending at position 0 of a node,\n    // like the one resulting from a triple click, is corrected so that it ends\n    // at the last position of the previous node instead.\n    const endLeaf = firstLeaf(end);\n    const beforeEnd = endLeaf.previousSibling;\n    if (\n        correctTripleClick &&\n        !endOffset &&\n        (start !== end || startOffset !== endOffset) &&\n        (!beforeEnd ||\n            (beforeEnd.nodeType === Node.TEXT_NODE &&\n                !isVisibleTextNode(beforeEnd) &&\n                !isZWS(beforeEnd))) &&\n        !closestElement(endLeaf, 'table')\n    ) {\n        const previous = previousLeaf(endLeaf, editable, true);\n        if (previous && closestElement(previous).isContentEditable) {\n            [end, endOffset] = [previous, nodeSize(previous)];\n        }\n    }\n\n    if (select) {\n        if (isBackwards) {\n            [start, end, startOffset, endOffset] = [end, start, endOffset, startOffset];\n            range.setEnd(start, startOffset);\n            range.collapse(false);\n        } else {\n            range.setStart(start, startOffset);\n            range.collapse(true);\n        }\n        sel.removeAllRanges();\n        sel.addRange(range);\n        try {\n            sel.extend(end, endOffset);\n        } catch {\n            // Firefox yells not happy when setting selection on elem with contentEditable=false.\n        }\n        range = sel.getRangeAt(0);\n    } else {\n        range.setStart(start, startOffset);\n        range.setEnd(end, endOffset);\n    }\n    return range;\n}\n\nexport function getAdjacentCharacter(editable, side) {\n    let { focusNode, focusOffset } = editable.ownerDocument.getSelection();\n    const originalBlock = closestBlock(focusNode);\n    let adjacentCharacter;\n    while (!adjacentCharacter && focusNode) {\n        if (side === 'previous') {\n            adjacentCharacter = focusOffset > 0 && focusNode.textContent[focusOffset - 1];\n        } else {\n            adjacentCharacter = focusNode.textContent[focusOffset];\n        }\n        if (!adjacentCharacter) {\n            if (side === 'previous') {\n                focusNode = previousLeaf(focusNode, editable);\n                focusOffset = focusNode && nodeSize(focusNode);\n            } else {\n                focusNode = nextLeaf(focusNode, editable);\n                focusOffset = 0;\n            }\n            const characterIndex = side === 'previous' ? focusOffset - 1 : focusOffset;\n            adjacentCharacter = focusNode && focusNode.textContent[characterIndex];\n        }\n    }\n    return closestBlock(focusNode) === originalBlock ? adjacentCharacter : undefined;\n}\n\nfunction isZwnbsp(node) {\n    return node.nodeType === Node.TEXT_NODE && node.textContent === '\\ufeff';\n}\n\nfunction isTangible(node) {\n    return isVisible(node) || isZwnbsp(node) || hasTangibleContent(node);\n}\n\nfunction hasTangibleContent(node) {\n    return [...(node?.childNodes || [])].some(n => isTangible(n));\n}\n\nexport function getDeepestPosition(node, offset) {\n    let direction = DIRECTIONS.RIGHT;\n    let next = node;\n    while (next) {\n        if (isTangible(next) || isZWS(next)) {\n            // Valid node: update position then try to go deeper.\n            if (next !== node) {\n                [node, offset] = [next, direction ? 0 : nodeSize(next)];\n            }\n            // First switch direction to left if offset is at the end.\n            direction = offset < node.childNodes.length;\n            next = node.childNodes[direction ? offset : offset - 1];\n        } else if (\n            direction &&\n            next.nextSibling &&\n            closestBlock(node).contains(next.nextSibling)\n        ) {\n            // Invalid node: skip to next sibling (without crossing blocks).\n            next = next.nextSibling;\n        } else {\n            // Invalid node: skip to previous sibling (without crossing blocks).\n            direction = DIRECTIONS.LEFT;\n            next = closestBlock(node).contains(next.previousSibling) && next.previousSibling;\n        }\n        // Avoid too-deep ranges inside self-closing elements like [BR, 0].\n        next = !isSelfClosingElement(next) && next;\n    }\n    return [node, offset];\n}\n\nexport function getCursors(document) {\n    const sel = document.getSelection();\n    if (\n        getCursorDirection(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) ===\n        DIRECTIONS.LEFT\n    )\n        return [\n            [sel.focusNode, sel.focusOffset],\n            [sel.anchorNode, sel.anchorOffset],\n        ];\n    return [\n        [sel.anchorNode, sel.anchorOffset],\n        [sel.focusNode, sel.focusOffset],\n    ];\n}\n\nexport function preserveCursor(document) {\n    const sel = document.getSelection();\n    const cursorPos = [sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset];\n    return replace => {\n        replace = replace || new Map();\n        cursorPos[0] = replace.get(cursorPos[0]) || cursorPos[0];\n        cursorPos[2] = replace.get(cursorPos[2]) || cursorPos[2];\n        return setSelection(...cursorPos, false);\n    };\n}\n\n/**\n * Check if the selection starts inside given selector. This function can be\n * used as the `isDisabled` property of a command of the PowerBox to disable\n * a command in the given selectors.\n * @param {string}: comma separated string with all the desired selectors\n * @returns {boolean} true selector is within one of the selector\n * (if the command should be filtered)\n */\nexport function isSelectionInSelectors(selector) {\n    let anchor = document.getSelection().anchorNode;\n    if (anchor && anchor.nodeType && anchor.nodeType !== Node.ELEMENT_NODE) {\n        anchor = anchor.parentElement;\n    }\n    if (anchor && closestElement(anchor, selector)) {\n        return true;\n    }\n    return false;\n}\n\nexport function getOffsetAndCharSize(nodeValue, offset, direction) {\n    //We get the correct offset which corresponds to this offset\n    // If direction is left it means we are coming from the right and\n    // we want to get the end offset of the first element to the left\n    // Example with LEFT direction:\n    // <p>a \\uD83D[offset]\\uDE0D b</p> -> <p>a \\uD83D\\uDE0D[offset] b</p> and\n    // size = 2 so delete backward will delete the whole emoji.\n    // Example with Right direction:\n    // <p>a \\uD83D[offset]\\uDE0D b</p> -> <p>a [offset]\\uD83D\\uDE0D b</p> and\n    // size = 2 so delete forward will delete the whole emoji.\n    const splittedNodeValue = [...nodeValue];\n    let charSize = 1;\n    let newOffset = offset;\n    let currentSize = 0;\n    for (const item of splittedNodeValue) {\n        currentSize += item.length;\n        if (currentSize >= offset) {\n            newOffset = direction == DIRECTIONS.LEFT ? currentSize : currentSize - item.length;\n            charSize = item.length;\n            break;\n        }\n    }\n    return [newOffset, charSize];\n}\n\n//------------------------------------------------------------------------------\n// Format utils\n//------------------------------------------------------------------------------\n\nexport const formatsSpecs = {\n    italic: {\n        tagName: 'em',\n        isFormatted: isItalic,\n        isTag: (node) => ['EM', 'I'].includes(node.tagName),\n        hasStyle: (node) => Boolean(node.style && node.style['font-style']),\n        addStyle: (node) => node.style['font-style'] = 'italic',\n        addNeutralStyle: (node) => node.style['font-style'] = 'normal',\n        removeStyle: (node) => removeStyle(node, 'font-style'),\n    },\n    bold: {\n        tagName: 'strong',\n        isFormatted: isBold,\n        isTag: (node) => ['STRONG', 'B'].includes(node.tagName),\n        hasStyle: (node) => Boolean(node.style && node.style['font-weight']),\n        addStyle: (node) => node.style['font-weight'] = 'bolder',\n        addNeutralStyle: (node) => {\n            node.style['font-weight'] = 'normal'\n        },\n        removeStyle: (node) => removeStyle(node, 'font-weight'),\n    },\n    underline: {\n        tagName: 'u',\n        isFormatted: isUnderline,\n        isTag: (node) => node.tagName === 'U',\n        hasStyle: (node) => node.style && node.style['text-decoration-line'].includes('underline'),\n        addStyle: (node) => node.style['text-decoration-line'] += ' underline',\n        removeStyle: (node) => removeStyle(node, 'text-decoration-line', 'underline'),\n    },\n    strikeThrough: {\n        tagName: 's',\n        isFormatted: isStrikeThrough,\n        isTag: (node) => node.tagName === 'S',\n        hasStyle: (node) => node.style && node.style['text-decoration-line'].includes('line-through'),\n        addStyle: (node) => node.style['text-decoration-line'] += ' line-through',\n        removeStyle: (node) => removeStyle(node, 'text-decoration-line', 'line-through'),\n    },\n    fontSize: {\n        isFormatted: isFontSize,\n        hasStyle: (node) => node.style && node.style['font-size'],\n        addStyle: (node, props) => {\n            node.style['font-size'] = props.size;\n            node.classList.remove(...FONT_SIZE_CLASSES);\n        },\n        removeStyle: (node) => removeStyle(node, 'font-size'),\n    },\n    setFontSizeClassName: {\n        isFormatted: hasClass,\n        hasStyle: (node, props) => FONT_SIZE_CLASSES\n            .find(cls => node.classList.contains(cls)),\n        addStyle: (node, props) => node.classList.add(props.className),\n        removeStyle: (node) => {\n            node.classList.remove(...FONT_SIZE_CLASSES, ...TEXT_STYLE_CLASSES);\n            if (node.classList.length === 0) {\n                node.removeAttribute(\"class\");\n            }\n        },\n    },\n    switchDirection: {\n        isFormatted: isDirectionSwitched,\n    }\n}\n\nconst removeStyle = (node, styleName, item) => {\n    if (item) {\n        const newStyle = node.style[styleName].split(' ').filter(x => x !== item).join(' ');\n        node.style[styleName] = newStyle || null;\n    } else {\n        node.style[styleName] = null;\n    }\n    if (node.getAttribute('style') === '') {\n        node.removeAttribute('style');\n    }\n};\nconst getOrCreateSpan = (node, ancestors) => {\n    const span = ancestors.find((element) => element.tagName === 'SPAN' && element.isConnected);\n    if (span) {\n        return span;\n    } else {\n        const span = document.createElement('span');\n        node.after(span);\n        span.append(node);\n        return span;\n    }\n}\nconst removeFormat = (node, formatSpec) => {\n    node = closestElement(node);\n    if (formatSpec.hasStyle(node)) {\n        formatSpec.removeStyle(node);\n        if (['SPAN', 'FONT'].includes(node.tagName) && !node.getAttributeNames().length) {\n            return unwrapContents(node);\n        }\n    }\n\n    if (formatSpec.isTag && formatSpec.isTag(node)) {\n        const attributesNames = node.getAttributeNames().filter((name)=> {\n            return name !== 'data-oe-zws-empty-inline';\n        });\n        if (attributesNames.length) {\n            // Change tag name\n            const newNode = document.createElement('span');\n            while (node.firstChild) {\n                newNode.appendChild(node.firstChild);\n            }\n            for (let index = node.attributes.length - 1; index >= 0; --index) {\n                newNode.attributes.setNamedItem(node.attributes[index].cloneNode());\n            }\n            node.parentNode.replaceChild(newNode, node);\n        } else {\n            unwrapContents(node);\n        }\n    }\n}\n\nexport const formatSelection = (editor, formatName, {applyStyle, formatProps} = {}) => {\n    const selection = editor.document.getSelection();\n    let direction\n    let wasCollapsed;\n    if (editor.editable.querySelector('.o_selected_td')) {\n        direction = DIRECTIONS.RIGHT;\n    } else {\n        if (!selection.rangeCount) return;\n        wasCollapsed = selection.getRangeAt(0).collapsed;\n\n        direction = getCursorDirection(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);\n    }\n    getDeepRange(editor.editable, { splitText: true, select: true, correctTripleClick: true });\n\n    if (typeof applyStyle === 'undefined') {\n        applyStyle = !isSelectionFormat(editor.editable, formatName);\n    }\n\n    let zws;\n    if (wasCollapsed) {\n        if (selection.anchorNode.nodeType === Node.TEXT_NODE && selection.anchorNode.textContent === '\\u200b') {\n            zws = selection.anchorNode;\n            selection.getRangeAt(0).selectNode(zws);\n        } else {\n            zws = insertAndSelectZws(selection);\n        }\n        getDeepRange(editor.editable, { splitText: true, select: true, correctTripleClick: true });\n    }\n\n    // Get selected nodes within td to handle non-p elements like h1, h2...\n    // Targeting <br> to ensure span stays inside its corresponding block node.\n    const selectedNodesInTds = [...editor.editable.querySelectorAll('.o_selected_td')]\n        .map(node => closestElement(node).querySelector('br'));\n    const selectedNodes = getSelectedNodes(editor.editable)\n        .filter(n => n.nodeType === Node.TEXT_NODE && closestElement(n).isContentEditable && (isVisibleTextNode(n) || isZWS(n)));\n    const selectedTextNodes = selectedNodes.length ? selectedNodes : selectedNodesInTds;\n\n    const selectedFieldNodes = new Set(getSelectedNodes(editor.editable)\n            .map(n =>closestElement(n, \"*[t-field],*[t-out],*[t-esc]\"))\n            .filter(Boolean));\n\n    const formatSpec = formatsSpecs[formatName];\n    for (const selectedTextNode of selectedTextNodes) {\n        const inlineAncestors = [];\n        let currentNode = selectedTextNode;\n        let parentNode = selectedTextNode.parentElement;\n\n        // Remove the format on all inline ancestors until a block or an element\n        // with a class that is not related to font size (in case the formatting\n        // comes from the class).\n        while (\n            parentNode && !isBlock(parentNode) &&\n            !isUnbreakable(parentNode) && !isUnbreakable(currentNode) &&\n            (parentNode.classList.length === 0 ||\n                [...parentNode.classList].every(cls => FONT_SIZE_CLASSES.includes(cls)))\n        ) {\n            const isUselessZws = parentNode.tagName === 'SPAN' &&\n                parentNode.hasAttribute('data-oe-zws-empty-inline') &&\n                parentNode.getAttributeNames().length === 1;\n\n            if (isUselessZws) {\n                unwrapContents(parentNode);\n            } else {\n                const newLastAncestorInlineFormat = splitAroundUntil(currentNode, parentNode);\n                removeFormat(newLastAncestorInlineFormat, formatSpec);\n                if (newLastAncestorInlineFormat.isConnected) {\n                    inlineAncestors.push(newLastAncestorInlineFormat);\n                    currentNode = newLastAncestorInlineFormat;\n                }\n            }\n\n            parentNode = currentNode.parentElement;\n        }\n\n        const firstBlockOrClassHasFormat = formatSpec.isFormatted(parentNode, formatProps);\n        if (firstBlockOrClassHasFormat && !applyStyle) {\n            formatSpec.addNeutralStyle && formatSpec.addNeutralStyle(getOrCreateSpan(selectedTextNode, inlineAncestors));\n        } else if (!firstBlockOrClassHasFormat && applyStyle) {\n            const tag = formatSpec.tagName && document.createElement(formatSpec.tagName);\n            if (tag) {\n                selectedTextNode.after(tag);\n                tag.append(selectedTextNode);\n\n                if (!formatSpec.isFormatted(tag, formatProps)) {\n                    tag.after(selectedTextNode);\n                    tag.remove();\n                    formatSpec.addStyle(getOrCreateSpan(selectedTextNode, inlineAncestors), formatProps);\n                }\n            } else if (formatName !== 'fontSize' || formatProps.size !== undefined) {\n                formatSpec.addStyle(getOrCreateSpan(selectedTextNode, inlineAncestors), formatProps);\n            }\n        }\n    }\n\n    for (const selectedFieldNode of selectedFieldNodes) {\n        if (applyStyle) {\n            formatSpec.addStyle(selectedFieldNode, formatProps);\n        } else {\n            formatSpec.removeStyle(selectedFieldNode);\n        }\n    }\n\n    if (zws) {\n        const siblings = [...zws.parentElement.childNodes];\n        if (\n            !isBlock(zws.parentElement) &&\n            selectedTextNodes.includes(siblings[0]) &&\n            selectedTextNodes.includes(siblings[siblings.length - 1])\n        ) {\n            zws.parentElement.setAttribute('data-oe-zws-empty-inline', '');\n        } else {\n            const span = document.createElement('span');\n            span.setAttribute('data-oe-zws-empty-inline', '');\n            zws.before(span);\n            span.append(zws);\n        }\n    }\n\n    if (selectedTextNodes[0] && selectedTextNodes[0].textContent === '\\u200B') {\n        setSelection(selectedTextNodes[0], 0);\n    } else if (selectedTextNodes.length) {\n        const firstNode = selectedTextNodes[0];\n        const lastNode = selectedTextNodes[selectedTextNodes.length - 1];\n        if (direction === DIRECTIONS.RIGHT) {\n            setSelection(firstNode, 0, lastNode, lastNode.length, false);\n        } else {\n            setSelection(lastNode, lastNode.length, firstNode, 0, false);\n        }\n    }\n}\nexport const isLinkEligibleForZwnbsp = (editable, link) => {\n    return link.isContentEditable && editable.contains(link) && !(\n        [link, ...link.querySelectorAll('*')].some(el => el.nodeName === 'IMG' || isBlock(el)) ||\n        link.matches('nav a, a.nav-link')\n    );\n}\n/**\n * Take a link and pad it with non-break zero-width spaces to ensure that it is\n * always possible to place the cursor at its inner and outer edges.\n *\n * @param {HTMLElement} editable\n * @param {HTMLAnchorElement} link\n */\nexport const padLinkWithZws = (editable, link) => {\n    if (!isLinkEligibleForZwnbsp(editable, link)) {\n        // Only add the ZWNBSP for simple (possibly styled) text links, and\n        // never in a nav.\n        return;\n    }\n    const selection = editable.ownerDocument.getSelection() || {};\n    const { anchorOffset, focusOffset } = selection;\n    let extraAnchorOffset = 0;\n    let extraFocusOffset = 0;\n    if (!link.textContent.startsWith('\\uFEFF')) {\n        if (selection.anchorNode === link && anchorOffset) {\n            extraAnchorOffset += 1;\n        }\n        if (selection.focusNode === link && focusOffset) {\n            extraFocusOffset += 1;\n        }\n        link.prepend(document.createTextNode('\\uFEFF'));\n    }\n    if (!link.textContent.endsWith('\\uFEFF')) {\n        if (selection.anchorNode === link && anchorOffset + extraAnchorOffset === nodeSize(link)) {\n            extraAnchorOffset += 1;\n        }\n        if (selection.focusNode === link && focusOffset + extraFocusOffset === nodeSize(link)) {\n            extraFocusOffset += 1;\n        }\n        link.append(document.createTextNode('\\uFEFF'));\n    }\n    const linkIndex = childNodeIndex(link);\n    if (!(link.previousSibling && link.previousSibling.textContent.endsWith('\\uFEFF'))) {\n        if (selection.anchorNode === link.parentElement && anchorOffset + extraAnchorOffset > linkIndex) {\n            extraAnchorOffset += 1;\n        }\n        if (selection.focusNode === link.parentElement && focusOffset + extraFocusOffset > linkIndex) {\n            extraFocusOffset += 1;\n        }\n        link.before(document.createTextNode('\\uFEFF'));\n    }\n    if (!(link.nextSibling && link.nextSibling.textContent.startsWith('\\uFEFF'))) {\n        if (selection.anchorNode === link.parentElement && anchorOffset + extraAnchorOffset > linkIndex + 1) {\n            extraAnchorOffset += 1;\n        }\n        if (selection.focusNode === link.parentElement && focusOffset + extraFocusOffset > linkIndex + 1) {\n            extraFocusOffset += 1;\n        }\n        link.after(document.createTextNode('\\uFEFF'));\n    }\n    if (extraAnchorOffset || extraFocusOffset) {\n        setSelection(\n            selection.anchorNode, anchorOffset + extraAnchorOffset,\n            selection.focusNode, focusOffset + extraFocusOffset,\n        );\n    }\n}\n\n//------------------------------------------------------------------------------\n// DOM Info utils\n//------------------------------------------------------------------------------\n\n/**\n * The following is a complete list of all HTML \"block-level\" elements.\n *\n * Source:\n * https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements\n *\n **/\nconst blockTagNames = [\n    'ADDRESS',\n    'ARTICLE',\n    'ASIDE',\n    'BLOCKQUOTE',\n    'DETAILS',\n    'DIALOG',\n    'DD',\n    'DIV',\n    'DL',\n    'DT',\n    'FIELDSET',\n    'FIGCAPTION',\n    'FIGURE',\n    'FOOTER',\n    'FORM',\n    'H1',\n    'H2',\n    'H3',\n    'H4',\n    'H5',\n    'H6',\n    'HEADER',\n    'HGROUP',\n    'HR',\n    'LI',\n    'MAIN',\n    'NAV',\n    'OL',\n    'P',\n    'PRE',\n    'SECTION',\n    'TABLE',\n    'UL',\n    // The following elements are not in the W3C list, for some reason.\n    'SELECT',\n    'OPTION',\n    'TR',\n    'TD',\n    'TBODY',\n    'THEAD',\n    'TH',\n];\nconst computedStyles = new WeakMap();\n/**\n * Return true if the given node is a block-level element, false otherwise.\n *\n * @param node\n */\nexport function isBlock(node) {\n    if (!node || node.nodeType !== Node.ELEMENT_NODE) {\n        return false;\n    }\n    const tagName = node.nodeName.toUpperCase();\n    // Every custom jw-* node will be considered as blocks.\n    if (\n        tagName.startsWith('JW-') ||\n        (tagName === 'T' &&\n            node.getAttribute('t-esc') === null &&\n            node.getAttribute('t-out') === null &&\n            node.getAttribute('t-raw') === null)\n    ) {\n        return true;\n    }\n    if (tagName === 'BR') {\n        // A <br> is always inline but getComputedStyle(br).display mistakenly\n        // returns 'block' if its parent is display:flex (at least on Chrome and\n        // Firefox (Linux)). Browsers normally support setting a <br>'s display\n        // property to 'none' but any other change is not supported. Therefore\n        // it is safe to simply declare that a <br> is never supposed to be a\n        // block.\n        return false;\n    }\n    // The node might not be in the DOM, in which case it has no CSS values.\n    if (!node.isConnected) {\n        return blockTagNames.includes(tagName);\n    }\n    // We won't call `getComputedStyle` more than once per node.\n    let style = computedStyles.get(node);\n    if (!style) {\n        style = node.ownerDocument.defaultView?.getComputedStyle(node);\n        computedStyles.set(node, style);\n    }\n    if (style?.display) {\n        return !style.display.includes('inline') && style.display !== 'contents';\n    }\n    return blockTagNames.includes(tagName);\n}\n\n/**\n * Return true if the given node appears bold. The node is considered to appear\n * bold if its font weight is bigger than 500 (eg.: Heading 1), or if its font\n * weight is bigger than that of its closest block.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isBold(node) {\n    const fontWeight = +getComputedStyle(closestElement(node)).fontWeight;\n    return fontWeight > 500 || fontWeight > +getComputedStyle(closestBlock(node)).fontWeight;\n}\n/**\n * Return true if the given node appears italic.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isItalic(node) {\n    return getComputedStyle(closestElement(node)).fontStyle === 'italic';\n}\n/**\n * Return true if the given node appears underlined.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isUnderline(node) {\n    let parent = closestElement(node);\n    while (parent) {\n        if (getComputedStyle(parent).textDecorationLine.includes('underline')) {\n            return true;\n        }\n        parent = parent.parentElement;\n    }\n    return false;\n}\n/**\n * Return true if the given node appears struck through.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isStrikeThrough(node) {\n    let parent = closestElement(node);\n    while (parent) {\n        if (getComputedStyle(parent).textDecorationLine.includes('line-through')) {\n            return true;\n        }\n        parent = parent.parentElement;\n    }\n    return false;\n}\n/**\n * Return true if the given node font-size is equal to `props.size`.\n *\n * @param {Object} props\n * @param {Node} props.node A node to compare the font-size against.\n * @param {String} props.size The font-size value of the node that will be\n *     checked against.\n * @returns {boolean}\n */\nexport function isFontSize(node, props) {\n    const element = closestElement(node);\n    return getComputedStyle(element)['font-size'] === props.size;\n}\n/**\n * Return true if the given node classlist contains `props.className`.\n *\n * @param {Object} props\n * @param {Node} node A node to compare the font-size against.\n * @param {String} props.className The name of the class.\n * @returns {boolean}\n */\nexport function hasClass(node, props) {\n    const element = closestElement(node);\n    return element.classList.contains(props.className);\n}\n/**\n * Return true if the given node appears in a different direction than that of\n * the editable ('ltr' or 'rtl').\n *\n * Note: The direction of the editable is set on its \"dir\" attribute, to the\n * value of the \"direction\" option on instantiation of the editor.\n *\n * @param {Node} node\n * @param {Element} editable\n * @returns {boolean}\n */\n export function isDirectionSwitched(node, editable) {\n    const defaultDirection = editable.getAttribute('dir');\n    return getComputedStyle(closestElement(node)).direction !== defaultDirection;\n}\n/**\n * Return true if the current selection on the editable appears as the given\n * format. The selection is considered to appear as that format if every text\n * node in it appears as that format.\n *\n * @param {Element} editable\n * @param {String} format 'bold'|'italic'|'underline'|'strikeThrough'|'switchDirection'\n * @returns {boolean}\n */\nexport function isSelectionFormat(editable, format) {\n    const selectedNodes = getTraversedNodes(editable)\n        .filter((n) => n.nodeType === Node.TEXT_NODE && n.nodeValue.replaceAll(ZWNBSP_CHAR, '').length);\n    const isFormatted = formatsSpecs[format].isFormatted;\n    return selectedNodes.length && selectedNodes.every(n => isFormatted(n, editable));\n}\n\nexport function isUnbreakable(node) {\n    if (!node || node.nodeType === Node.TEXT_NODE) {\n        return false;\n    }\n    if (node.nodeType !== Node.ELEMENT_NODE) {\n        return true;\n    }\n    return (\n        isUnremovable(node) || // An unremovable node is always unbreakable.\n        ['TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'SECTION', 'DIV'].includes(node.tagName) ||\n        node.hasAttribute('t') ||\n        (node.nodeType === Node.ELEMENT_NODE &&\n            (node.nodeName === 'T' ||\n                node.getAttribute('t-if') ||\n                node.getAttribute('t-esc') ||\n                node.getAttribute('t-elif') ||\n                node.getAttribute('t-else') ||\n                node.getAttribute('t-foreach') ||\n                node.getAttribute('t-value') ||\n                node.getAttribute('t-out') ||\n                node.getAttribute('t-raw')) ||\n                node.getAttribute('t-field')) ||\n        node.classList.contains('oe_unbreakable')\n    );\n}\n\nexport function isUnremovable(node) {\n    return (\n        (node.nodeType !== Node.COMMENT_NODE && node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) ||\n        node.oid === 'root' ||\n        (node.nodeType === Node.ELEMENT_NODE &&\n            (node.classList.contains('o_editable') || node.getAttribute('t-set') || node.getAttribute('t-call'))) ||\n        (node.classList && node.classList.contains('oe_unremovable')) ||\n        (node.nodeName === 'SPAN' && node.parentElement && node.parentElement.getAttribute('data-oe-type') === 'monetary') ||\n        (node.ownerDocument && node.ownerDocument.defaultWindow && !ancestors(node).find(ancestor => ancestor.oid === 'root')) // Node is in DOM but not in editable.\n    );\n}\n\nexport function containsUnbreakable(node) {\n    if (!node) {\n        return false;\n    }\n    return isUnbreakable(node) || containsUnbreakable(node.firstChild);\n}\n\nconst iconTags = ['I', 'SPAN'];\nconst iconClasses = ['fa', 'fab', 'fad', 'far', 'oi'];\n/**\n * Indicates if the given node is an icon element.\n *\n * @see ICON_SELECTOR\n * @param {?Node} [node]\n * @returns {boolean}\n */\nexport function isIconElement(node) {\n    return !!(\n        node &&\n        iconTags.includes(node.nodeName) &&\n        iconClasses.some(cls => node.classList.contains(cls))\n    );\n}\nexport const ICON_SELECTOR = iconTags.map(tag => {\n    return iconClasses.map(cls => {\n        return `${tag}.${cls}`;\n    }).join(', ');\n}).join(', ');\n\n/**\n * Return true if the given node is a zero-width breaking space (200b), false\n * otherwise. Note that this will return false for a zero-width NON-BREAK space\n * (feff)!\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isZWS(node) {\n    return (\n        node &&\n        node.textContent === '\\u200B'\n    );\n}\nexport function isEditorTab(node) {\n    return (\n        node &&\n        (node.nodeName === 'SPAN') &&\n        node.classList.contains('oe-tabs')\n    );\n}\nexport function isMediaElement(node) {\n    return (\n        isIconElement(node) ||\n        (node.classList &&\n            (node.classList.contains('o_image') || node.classList.contains('media_iframe_video')))\n    );\n}\n/**\n * A \"protected\" node will have its mutations filtered and not be registered\n * in an history step. Some editor features like selection handling, command\n * hint, toolbar, tooltip, etc. are also disabled. Protected roots have their\n * data-oe-protected attribute set to either \"\" or \"true\". If the closest parent\n * with a data-oe-protected attribute has the value \"false\", it is not\n * protected. Unknown values are ignored.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isProtected(node) {\n    const closestProtectedElement = closestElement(node, '[data-oe-protected]');\n    if (closestProtectedElement) {\n        return [\"\", \"true\"].includes(closestProtectedElement.dataset.oeProtected);\n    }\n    return false;\n}\n\n// https://developer.mozilla.org/en-US/docs/Glossary/Void_element\nconst VOID_ELEMENT_NAMES = ['AREA', 'BASE', 'BR', 'COL', 'EMBED', 'HR', 'IMG',\n    'INPUT', 'KEYGEN', 'LINK', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR'];\n\nexport function isArtificialVoidElement(node) {\n    return isMediaElement(node) || node.nodeName === 'HR';\n}\n\nexport function isNotAllowedContent(node) {\n    return isArtificialVoidElement(node) || VOID_ELEMENT_NAMES.includes(node.nodeName);\n}\n\nexport function containsUnremovable(node) {\n    if (!node) {\n        return false;\n    }\n    return isUnremovable(node) || containsUnremovable(node.firstChild);\n}\n\nexport function getInSelection(document, selector) {\n    const selection = document.getSelection();\n    const range = selection && !!selection.rangeCount && selection.getRangeAt(0);\n    if (range) {\n        const selectorInStartAncestors = closestElement(range.startContainer, selector);\n        if (selectorInStartAncestors) {\n            return selectorInStartAncestors;\n        } else {\n            const commonElementAncestor = closestElement(range.commonAncestorContainer);\n            return commonElementAncestor && [...commonElementAncestor.querySelectorAll(selector)].find(\n                node => range.intersectsNode(node),\n            );\n        }\n    }\n}\n\n/**\n * Get the index of the given table row/cell.\n *\n * @private\n * @param {HTMLTableRowElement|HTMLTableCellElement} trOrTd\n * @returns {number}\n */\nexport function getRowIndex(trOrTd) {\n    const tr = closestElement(trOrTd, 'tr');\n    const trParent = tr && tr.parentElement;\n    if (!trParent) {\n        return -1;\n    }\n    const trSiblings = [...trParent.children].filter(child => child.nodeName === 'TR');\n    return trSiblings.findIndex(child => child === tr);\n}\n\n/**\n * Get the index of the given table cell.\n *\n * @private\n * @param {HTMLTableCellElement} td\n * @returns {number}\n */\nexport function getColumnIndex(td) {\n    const tdParent = td.parentElement;\n    if (!tdParent) {\n        return -1;\n    }\n    const tdSiblings = [...tdParent.children].filter(child => child.nodeName === 'TD' || child.nodeName === 'TH');\n    return tdSiblings.findIndex(child => child === td);\n}\n\n// This is a list of \"paragraph-related elements\", defined as elements that\n// behave like paragraphs.\nexport const paragraphRelatedElements = [\n    'P',\n    'H1',\n    'H2',\n    'H3',\n    'H4',\n    'H5',\n    'H6',\n    'PRE',\n    'BLOCKQUOTE',\n];\n\n/**\n * Return true if the given node allows \"paragraph-related elements\".\n *\n * @see paragraphRelatedElements\n * @param {Node} node\n * @returns {boolean}\n */\nexport function allowsParagraphRelatedElements(node) {\n    return isBlock(node) && !paragraphRelatedElements.includes(node.nodeName);\n}\n\n/**\n * Take a node and unwrap all of its block contents recursively. All blocks\n * (except for firstChilds) are preceded by a <br> in order to preserve the line\n * breaks.\n *\n * @param {Node} node\n */\nexport function makeContentsInline(node) {\n    let childIndex = 0;\n    for (const child of node.childNodes) {\n        if (isBlock(child)) {\n            if (childIndex && paragraphRelatedElements.includes(child.nodeName)) {\n                child.before(document.createElement('br'));\n            }\n            for (const grandChild of child.childNodes) {\n                child.before(grandChild);\n                makeContentsInline(grandChild);\n            }\n            child.remove();\n        }\n        childIndex += 1;\n    }\n}\n\n// optimize: use the parent Oid to speed up detection\nexport function getOuid(node, optimize = false) {\n    while (node && !isUnbreakable(node)) {\n        if (node.ouid && optimize) return node.ouid;\n        node = node.parentNode;\n    }\n    return node && node.oid;\n}\n/**\n * Returns true if the provided node can suport html content.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isHtmlContentSupported(node) {\n    return !closestElement(node, '[data-oe-model]:not([data-oe-field=\"arch\"]):not([data-oe-type=\"html\"]),[data-oe-translation-id]', true);\n}\n/**\n * Returns whether the given node is a element that could be considered to be\n * removed by itself = self closing tags.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nconst selfClosingElementTags = ['BR', 'IMG', 'INPUT'];\nexport function isSelfClosingElement(node) {\n    return node && selfClosingElementTags.includes(node.nodeName);\n}\n/**\n * Returns true if the given node is in a PRE context for whitespace handling.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isInPre(node) {\n    const element = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;\n    return (\n        !!element &&\n        (!!element.closest('pre') ||\n            getComputedStyle(element).getPropertyValue('white-space') === 'pre')\n    );\n}\nconst whitespace = `[^\\\\S\\\\u00A0\\\\u0009\\\\ufeff]`; // for formatting (no \"real\" content) (TODO: 0009 shouldn't be included)\nconst whitespaceRegex = new RegExp(`^${whitespace}*$`);\nexport function isWhitespace(value) {\n    const str = typeof value === 'string' ? value : value.nodeValue;\n    return whitespaceRegex.test(str);\n}\n/**\n * Returns whether removing the given node from the DOM will have a visible\n * effect or not.\n *\n * Note: TODO this is not handling all cases right now, just the ones the\n * caller needs at the moment. For example a space text node between two inlines\n * will always return 'true' while it is sometimes invisible.\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isVisible(node) {\n    return !!node && (\n        (node.nodeType === Node.TEXT_NODE && isVisibleTextNode(node)) ||\n        (node.nodeType === Node.ELEMENT_NODE &&\n            (node.getAttribute(\"t-esc\") || node.getAttribute(\"t-out\"))) ||\n        isSelfClosingElement(node) ||\n        isIconElement(node) ||\n        hasVisibleContent(node)\n    );\n}\nexport function hasVisibleContent(node) {\n    return [...(node?.childNodes || [])].some(n => isVisible(n));\n}\nconst visibleCharRegex = /[^\\s\\u200b]|[\\u00A0\\u0009]$/; // contains at least a char that is always visible (TODO: 0009 shouldn't be included)\nexport function isVisibleTextNode(testedNode) {\n    if (!testedNode || !testedNode.length || testedNode.nodeType !== Node.TEXT_NODE) {\n        return false;\n    }\n    if (visibleCharRegex.test(testedNode.textContent) || (isInPre(testedNode) && isWhitespace(testedNode))) {\n        return true;\n    }\n    if (ZERO_WIDTH_CHARS.includes(testedNode.textContent)) {\n        return false; // a ZW(NB)SP is always invisible, regardless of context.\n    }\n    // The following assumes node is made entirely of whitespace and is not\n    // preceded of followed by a block.\n    // Find out contiguous preceding and following text nodes\n    let preceding;\n    let following;\n    // Control variable to know whether the current node has been found\n    let foundTestedNode;\n    const currentNodeParentBlock = closestBlock(testedNode);\n    if (!currentNodeParentBlock) {\n        return false;\n    }\n    const nodeIterator = document.createNodeIterator(currentNodeParentBlock);\n    for (let node = nodeIterator.nextNode(); node; node = nodeIterator.nextNode()) {\n        if (node.nodeType === Node.TEXT_NODE) {\n            // If we already found the tested node, the current node is the\n            // contiguous following, and we can stop looping\n            // If the current node is the tested node, mark it as found and\n            // continue.\n            // If we haven't reached the tested node, overwrite the preceding\n            // node.\n            if (foundTestedNode) {\n                following = node;\n                break;\n            } else if (testedNode === node) {\n                foundTestedNode = true;\n            } else {\n                preceding = node;\n            }\n        } else if (isBlock(node)) {\n            // If we found the tested node, then the following node is irrelevant\n            // If we didn't, then the current preceding node is irrelevant\n            if (foundTestedNode) {\n                break;\n            } else {\n                preceding = null;\n            }\n        } else if (foundTestedNode && !isWhitespace(node)) {\n            // <block>space<inline>text</inline></block> -> space is visible\n            following = node;\n            break;\n        }\n    }\n    while (following && !visibleCharRegex.test(following.textContent)) {\n        following = following.nextSibling;\n    }\n    // Missing preceding or following: invisible.\n    // Preceding or following not in the same block as tested node: invisible.\n    if (\n        !(preceding && following) ||\n        currentNodeParentBlock !== closestBlock(preceding) ||\n        currentNodeParentBlock !== closestBlock(following)\n    ) {\n        return false;\n    }\n    // Preceding is whitespace or following is whitespace: invisible\n    return visibleCharRegex.test(preceding.textContent);\n}\n\nexport function parentsGet(node, root = undefined) {\n    const parents = [];\n    while (node) {\n        parents.unshift(node);\n        if (node === root) {\n            break;\n        }\n        node = node.parentNode;\n    }\n    return parents;\n}\n\nexport function commonParentGet(node1, node2, root = undefined) {\n    if (!node1 || !node2) {\n        return null;\n    }\n    const n1p = parentsGet(node1, root);\n    const n2p = parentsGet(node2, root);\n    while (n1p.length > 1 && n1p[1] === n2p[1]) {\n        n1p.shift();\n        n2p.shift();\n    }\n    // Check  in case at least one of them is not in the DOM.\n    return n1p[0] === n2p[0] ? n1p[0] : null;\n}\n\nexport function getListMode(pnode) {\n    if (![\"UL\", \"OL\"].includes(pnode.tagName)) return;\n    if (pnode.tagName == 'OL') return 'OL';\n    return pnode.classList.contains('o_checklist') ? 'CL' : 'UL';\n}\n\nexport function createList(mode) {\n    const node = document.createElement(mode == 'OL' ? 'OL' : 'UL');\n    if (mode == 'CL') {\n        node.classList.add('o_checklist');\n    }\n    return node;\n}\n\nexport function insertListAfter(afterNode, mode, content = []) {\n    const list = createList(mode);\n    afterNode.after(list);\n    list.append(\n        ...content.map(c => {\n            const li = document.createElement('LI');\n            li.append(...[].concat(c));\n            return li;\n        }),\n    );\n    return list;\n}\n\nexport function toggleList(node, mode, offset = 0) {\n    let pnode = node.closest('ul, ol');\n    if (!pnode) return;\n    const listMode = getListMode(pnode) + mode;\n    if (['OLCL', 'ULCL'].includes(listMode)) {\n        pnode.classList.add('o_checklist');\n        for (let li = pnode.firstElementChild; li !== null; li = li.nextElementSibling) {\n            if (li.style.listStyle !== 'none') {\n                li.style.listStyle = null;\n                if (!li.style.all) li.removeAttribute('style');\n            }\n        }\n        pnode = setTagName(pnode, 'UL');\n    } else if (['CLOL', 'CLUL'].includes(listMode)) {\n        toggleClass(pnode, 'o_checklist');\n        pnode = setTagName(pnode, mode);\n    } else if (['OLUL', 'ULOL'].includes(listMode)) {\n        pnode = setTagName(pnode, mode);\n    } else {\n        // toggle => remove list\n        let currNode = node;\n        while (currNode) {\n            currNode = currNode.oShiftTab(offset);\n        }\n        return;\n    }\n    return pnode;\n}\n\n/**\n * Converts a list element and its nested elements to the specified list mode.\n *\n * @param {HTMLUListElement|HTMLOListElement|HTMLLIElement} node - HTML element\n * representing a list or list item.\n * @param {string} toMode - Target list mode\n * @returns {HTMLUListElement|HTMLOListElement|HTMLLIElement} node - Modified\n * list element after conversion.\n */\nexport function convertList(node, toMode) {\n    if (![\"UL\", \"OL\", \"LI\"].includes(node.nodeName)) return;\n    const listMode = getListMode(node);\n    if (listMode && toMode !== listMode) {\n        node = toggleList(node, toMode);\n    }\n    for (const child of node.childNodes) {\n        convertList(child, toMode);\n    }\n\n    return node;\n}\n\nexport function toggleClass(node, className) {\n    node.classList.toggle(className);\n    if (!node.className) {\n        node.removeAttribute('class');\n    }\n}\n\nexport function makeZeroWidthCharactersVisible(text) {\n    return text.replaceAll('\\u200B', '//ZWSP//').replaceAll('\\uFEFF', '//ZWNBSP//');\n}\n\n/**\n * Returns whether or not the given node is a BR element which does not really\n * act as a line break, but as a placeholder for the cursor or to make some left\n * element (like a space) visible.\n *\n * @param {HTMLBRElement} brEl\n * @returns {boolean}\n */\nexport function isFakeLineBreak(brEl) {\n    return !(getState(...rightPos(brEl), DIRECTIONS.RIGHT).cType & (CTYPES.CONTENT | CTGROUPS.BR));\n}\n/**\n * Checks whether or not the given block has any visible content, except for\n * a placeholder BR.\n *\n * @param {HTMLElement} blockEl\n * @returns {boolean}\n */\nexport function isEmptyBlock(blockEl) {\n    if (!blockEl || blockEl.nodeType !== Node.ELEMENT_NODE) {\n        return false;\n    }\n    if (isIconElement(blockEl) || visibleCharRegex.test(blockEl.textContent)) {\n        return false;\n    }\n    if (blockEl.querySelectorAll('br').length >= 2) {\n        return false;\n    }\n    const nodes = blockEl.querySelectorAll('*');\n    for (const node of nodes) {\n        // There is no text and no double BR, the only thing that could make\n        // this visible is a \"visible empty\" node like an image.\n        if (node.nodeName != 'BR' && (isSelfClosingElement(node) || isIconElement(node))) {\n            return false;\n        }\n    }\n    return true;\n}\n/**\n * Checks whether or not the given block element has something to make it have\n * a visible height (except for padding / border).\n *\n * @param {HTMLElement} blockEl\n * @returns {boolean}\n */\nexport function isShrunkBlock(blockEl) {\n    return (\n        isEmptyBlock(blockEl) &&\n        !blockEl.querySelector('br') &&\n        blockEl.nodeName !== \"IMG\"\n    );\n}\n\n/**\n * @param {string} [value]\n * @returns {boolean}\n */\nexport function isColorGradient(value) {\n    // FIXME duplicated in @web_editor/utils.js\n    return value && value.includes('-gradient(');\n}\n\n/**\n * Finds the font size to display for the current selection. We cannot rely\n * on the computed font-size only as font-sizes are responsive and we always\n * want to display the desktop (integer when possible) one.\n *\n * @private\n * @todo probably move `getCSSVariableValue` and `convertNumericToUnit` as\n *       odoo-editor utils.\n * @param {Selection} sel The current selection.\n * @returns {Float} The font size to display.\n */\nexport function getFontSizeDisplayValue(sel, getCSSVariableValue, convertNumericToUnit) {\n    const tagNameRelatedToFontSize = [\"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\"];\n    const styleClassesRelatedToFontSize = [\"display-1\", \"display-2\", \"display-3\", \"display-4\"];\n    const closestStartContainerEl = closestElement(sel.getRangeAt(0).startContainer);\n    const closestFontSizedEl = closestStartContainerEl.closest(`\n        [style*='font-size'],\n        ${FONT_SIZE_CLASSES.map(className => `.${className}`)},\n        ${styleClassesRelatedToFontSize.map(className => `.${className}`)},\n        ${tagNameRelatedToFontSize}\n    `);\n    let remValue;\n    if (closestFontSizedEl) {\n        const useFontSizeInput = closestFontSizedEl.style.fontSize;\n        if (useFontSizeInput) {\n            // Use the computed value to always convert to px. However, this\n            // currently does not check that the inline font-size is the one\n            // actually having an effect (there could be an !important CSS rule\n            // forcing something else).\n            // TODO align with the behavior of the rest of the editor snippet\n            // options.\n            return parseFloat(getComputedStyle(closestStartContainerEl).fontSize);\n        }\n        // It's a class font size or a hN tag. We don't return the computed\n        // font size because it can be different from the one displayed in\n        // the toolbar because it's responsive.\n        const fontSizeClass = FONT_SIZE_CLASSES.find(\n            className => closestFontSizedEl.classList.contains(className));\n        let fsName;\n        if (fontSizeClass) {\n            fsName = fontSizeClass.substring(0, fontSizeClass.length - 3); // Without -fs\n        } else {\n            fsName = styleClassesRelatedToFontSize.find(\n                    className => closestFontSizedEl.classList.contains(className))\n                || closestFontSizedEl.tagName.toLowerCase();\n        }\n        remValue = parseFloat(getCSSVariableValue(`${fsName}-font-size`));\n    }\n    // It's default font size (no font size class / style).\n    if (remValue === undefined) {\n        remValue = parseFloat(getCSSVariableValue(\"font-size-base\"));\n    }\n    const pxValue = convertNumericToUnit(remValue, \"rem\", \"px\");\n    return pxValue || parseFloat(getComputedStyle(closestStartContainerEl).fontSize);\n}\n\n//------------------------------------------------------------------------------\n// DOM Modification\n//------------------------------------------------------------------------------\n\n/**\n * Splits a text node in two parts.\n * If the split occurs at the beginning or the end, the text node stays\n * untouched and unsplit. If a split actually occurs, the original text node\n * still exists and become the right part of the split.\n *\n * Note: if split after or before whitespace, that whitespace may become\n * invisible, it is up to the caller to replace it by nbsp if needed.\n *\n * @param {Node} textNode\n * @param {number} offset\n * @param {DIRECTIONS} originalNodeSide Whether the original node ends up on left\n * or right after the split\n * @returns {number} The parentOffset if the cursor was between the two text\n *          node parts after the split.\n */\nexport function splitTextNode(textNode, offset, originalNodeSide = DIRECTIONS.RIGHT) {\n    let parentOffset = childNodeIndex(textNode);\n\n    if (offset > 0) {\n        parentOffset++;\n\n        if (offset < textNode.length) {\n            const left = textNode.nodeValue.substring(0, offset);\n            const right = textNode.nodeValue.substring(offset);\n            if (originalNodeSide === DIRECTIONS.LEFT) {\n                const newTextNode = document.createTextNode(right);\n                textNode.after(newTextNode);\n                textNode.nodeValue = left;\n            } else {\n                const newTextNode = document.createTextNode(left);\n                textNode.before(newTextNode);\n                textNode.nodeValue = right;\n            }\n        }\n    }\n    return parentOffset;\n}\n\n/**\n * Split the given element at the given offset. The element will be removed in\n * the process so caution is advised in dealing with its reference. Returns a\n * tuple containing the new elements on both sides of the split.\n *\n * @param {Element} element\n * @param {number} offset\n * @returns {[Element, Element]}\n */\nexport function splitElement(element, offset) {\n    const before = element.cloneNode();\n    const after = element.cloneNode();\n    let index = 0;\n    for (const child of [...element.childNodes]) {\n        index < offset ? before.appendChild(child) : after.appendChild(child);\n        index++;\n    }\n    // e.g.: <p>Test/banner</p> + ENTER <=> <p>Test</p><div class=\"o_editor_banner>...</div><p><br></p>\n    const blockEl = closestBlock(after);\n    if (blockEl) {\n        fillEmpty(blockEl);\n    }\n    element.before(before);\n    element.after(after);\n    element.remove();\n    return [before, after];\n}\n\n/**\n * Split around the given elements, until a given ancestor (included). Elements\n * will be removed in the process so caution is advised in dealing with their\n * references. Returns the new split root element that is a clone of\n * limitAncestor or the original limitAncestor if no split occured.\n *\n * @see splitElement\n * @param {Node[] | Node} elements\n * @param {Node} limitAncestor\n * @returns {[Node, Node]}\n */\nexport function splitAroundUntil(elements, limitAncestor) {\n    elements = Array.isArray(elements) ? elements : [elements];\n    const firstNode = elements[0];\n    const lastNode = elements[elements.length - 1];\n    if ([firstNode, lastNode].includes(limitAncestor)) {\n        return limitAncestor;\n    }\n    let before = firstNode.previousSibling;\n    let after = lastNode.nextSibling;\n    let beforeSplit, afterSplit;\n    if (!before && !after && elements[0] !== limitAncestor) {\n        return splitAroundUntil(elements[0].parentElement, limitAncestor);\n    }\n    // Split up ancestors up to font\n    while (after && after.parentElement !== limitAncestor) {\n        afterSplit = splitElement(after.parentElement, childNodeIndex(after))[0];\n        after = afterSplit.nextSibling;\n    }\n    if (after) {\n        afterSplit = splitElement(limitAncestor, childNodeIndex(after))[0];\n        limitAncestor = afterSplit;\n    }\n    while (before && before.parentElement !== limitAncestor) {\n        beforeSplit = splitElement(before.parentElement, childNodeIndex(before) + 1)[1];\n        before = beforeSplit.previousSibling;\n    }\n    if (before) {\n        beforeSplit = splitElement(limitAncestor, childNodeIndex(before) + 1)[1];\n    }\n    return beforeSplit || afterSplit || limitAncestor;\n}\n\nexport function insertText(sel, content) {\n    if (!content) {\n        return;\n    }\n    if (sel.anchorNode.nodeType === Node.TEXT_NODE) {\n        const pos = [sel.anchorNode.parentElement, splitTextNode(sel.anchorNode, sel.anchorOffset)];\n        setSelection(...pos, ...pos, false);\n    }\n    const txt = document.createTextNode(content || '#');\n    const restore = prepareUpdate(sel.anchorNode, sel.anchorOffset);\n    sel.getRangeAt(0).insertNode(txt);\n    restore();\n    setSelection(...boundariesOut(txt), false);\n    return txt;\n}\n\n/**\n * Inserts the given characters at the given offset of the given node.\n *\n * @param {string} chars\n * @param {Node} node\n * @param {number} offset\n */\nexport function insertCharsAt(chars, node, offset) {\n    if (node.nodeType === Node.TEXT_NODE) {\n        const startValue = node.nodeValue;\n        if (offset < 0 || offset > startValue.length) {\n            throw new Error(`Invalid ${chars} insertion in text node`);\n        }\n        node.nodeValue = startValue.slice(0, offset) + chars + startValue.slice(offset);\n    } else {\n        if (offset < 0 || offset > node.childNodes.length) {\n            throw new Error(`Invalid ${chars} insertion in non-text node`);\n        }\n        const textNode = document.createTextNode(chars);\n        if (offset < node.childNodes.length) {\n            node.insertBefore(textNode, node.childNodes[offset]);\n        } else {\n            node.appendChild(textNode);\n        }\n    }\n}\n\n/**\n * Remove node from the DOM while preserving their contents if any.\n *\n * @param {Node} node\n * @returns {Node[]}\n */\nexport function unwrapContents(node) {\n    const contents = [...node.childNodes];\n    for (const child of contents) {\n        node.parentNode.insertBefore(child, node);\n    }\n    node.parentNode.removeChild(node);\n    return contents;\n}\n\n/**\n * Add a BR in the given node if its closest ancestor block has nothing to make\n * it visible, and/or add a zero-width space in the given node if it's an empty\n * inline unremovable so the cursor can stay in it.\n *\n * @param {HTMLElement} el\n * @returns {Object} { br: the inserted <br> if any,\n *                     zws: the inserted zero-width space if any }\n */\nexport function fillEmpty(el) {\n    const fillers = {};\n    const blockEl = closestBlock(el);\n    if (isShrunkBlock(blockEl)) {\n        const br = document.createElement('br');\n        blockEl.appendChild(br);\n        fillers.br = br;\n    }\n    if (!isTangible(el) && !el.hasAttribute(\"data-oe-zws-empty-inline\") && !el.hasChildNodes()) {\n        // As soon as there is actual content in the node, the zero-width space\n        // is removed by the sanitize function.\n        const zws = document.createTextNode('\\u200B');\n        el.appendChild(zws);\n        el.setAttribute(\"data-oe-zws-empty-inline\", \"\");\n        fillers.zws = zws;\n        const previousSibling = el.previousSibling;\n        if (previousSibling && previousSibling.nodeName === \"BR\") {\n            previousSibling.remove();\n        }\n        setSelection(zws, 0, zws, 0);\n    }\n    // If the element is empty and inside an <a> tag with 'inline' display,\n    // it's not possible to place the cursor in element even if it contains\n    // ZWSP. To make the element cursor-friendly, change its display to\n    // 'inline-block'.\n    if (\n        !isVisible(el) &&\n        el.nodeName !== 'A' &&\n        closestElement(el, 'a') &&\n        getComputedStyle(el).display === 'inline'\n    ) {\n        el.style.display = 'inline-block';\n    }\n    return fillers;\n}\n/**\n * Takes a selection (assumed to be collapsed) and insert a zero-width space at\n * its anchor point. Then, select that zero-width space.\n *\n * @param {Selection} selection\n * @returns {Node} the inserted zero-width space\n */\nexport function insertAndSelectZws(selection) {\n    const offset = selection.anchorOffset;\n    const zws = insertText(selection, '\\u200B');\n    splitTextNode(zws, offset);\n    selection.getRangeAt(0).selectNode(zws);\n    return zws;\n}\n\nexport function setTagName(el, newTagName) {\n    if (el.tagName === newTagName) {\n        return el;\n    }\n    const n = document.createElement(newTagName);\n    if (el.nodeName !== 'LI') {\n        el.style.removeProperty('list-style');\n        const attributes = el.attributes;\n        for (const attr of attributes) {\n            n.setAttribute(attr.name, attr.value);\n        }\n    }\n    while (el.firstChild) {\n        n.append(el.firstChild);\n    }\n    if (el.tagName === 'LI') {\n        el.append(n);\n    } else {\n        el.parentNode.replaceChild(n, el);\n    }\n    return n;\n}\n/**\n * Moves the given subset of nodes of a source element to the given destination.\n * If the source element is left empty it is removed. This ensures the moved\n * content and its destination surroundings are restored (@see restoreState) to\n * the way there were.\n *\n * It also reposition at the right position on the left of the moved nodes.\n *\n * @param {HTMLElement} destinationEl\n * @param {number} destinationOffset\n * @param {HTMLElement} sourceEl\n * @param {number} [startIndex=0]\n * @param {number} [endIndex=sourceEl.childNodes.length]\n * @returns {Array.<HTMLElement, number} The position at the left of the moved\n *     nodes after the move was done (and where the cursor was returned).\n */\nexport function moveNodes(\n    destinationEl,\n    destinationOffset,\n    sourceEl,\n    startIndex = 0,\n    endIndex = sourceEl.childNodes.length,\n) {\n    if (selfClosingElementTags.includes(destinationEl.nodeName)) {\n        throw new Error(`moveNodes: Invalid destination element ${destinationEl.nodeName}`);\n    }\n\n    const nodes = [];\n    for (let i = startIndex; i < endIndex; i++) {\n        nodes.push(sourceEl.childNodes[i]);\n    }\n\n    if (nodes.length) {\n        const restoreDestination = prepareUpdate(destinationEl, destinationOffset);\n        const restoreMoved = prepareUpdate(\n            ...leftPos(sourceEl.childNodes[startIndex]),\n            ...rightPos(sourceEl.childNodes[endIndex - 1]),\n        );\n        const fragment = document.createDocumentFragment();\n        nodes.forEach(node => fragment.appendChild(node));\n        const posRightNode = destinationEl.childNodes[destinationOffset];\n        if (posRightNode) {\n            destinationEl.insertBefore(fragment, posRightNode);\n        } else {\n            destinationEl.appendChild(fragment);\n        }\n        restoreDestination();\n        restoreMoved();\n    }\n\n    if (!nodeSize(sourceEl)) {\n        const restoreOrigin = prepareUpdate(...boundariesOut(sourceEl));\n        sourceEl.remove();\n        restoreOrigin();\n    }\n\n    // Return cursor position, but don't change it\n    const firstNode = nodes.find(node => !!node.parentNode);\n    return firstNode ? leftPos(firstNode) : [destinationEl, destinationOffset];\n}\n/**\n * Remove ouid of a node and it's descendants in order to allow that tree\n * to be moved into another parent.\n */\nexport function resetOuids(node) {\n    node.ouid = undefined;\n    for (const descendant of descendants(node)) {\n        descendant.ouid = undefined;\n    }\n}\n\n//------------------------------------------------------------------------------\n// Prepare / Save / Restore state utilities\n//------------------------------------------------------------------------------\n\nconst prepareUpdateLockedEditables = new Set();\n/**\n * Any editor command is applied to a selection (collapsed or not). After the\n * command, the content type on the selection boundaries, in both direction,\n * should be preserved (some whitespace should disappear as went from collapsed\n * to non collapsed, or converted to &nbsp; as went from non collapsed to\n * collapsed, there also <br> to remove/duplicate, etc).\n *\n * This function returns a callback which allows to do that after the command\n * has been done.\n *\n * Note: the method has been made generic enough to work with non-collapsed\n * selection but can be used for an unique cursor position.\n *\n * @param {HTMLElement} el\n * @param {number} offset\n * @param {...(HTMLElement|number)} args - argument 1 and 2 can be repeated for\n *     multiple preparations with only one restore callback returned. Note: in\n *     that case, the positions should be given in the document node order.\n * @param {Object} [options]\n * @param {boolean} [options.allowReenter = true] - if false, all calls to\n *     prepareUpdate before this one gets restored will be ignored.\n * @param {string} [options.label = <random 6 character string>]\n * @param {boolean} [options.debug = false] - if true, adds nicely formatted\n *     console logs to help with debugging.\n * @returns {function}\n */\nexport function prepareUpdate(...args) {\n    const closestRoot = args.length && ancestors(args[0]).find(ancestor => ancestor.oid === 'root');\n    const isPrepareUpdateLocked = closestRoot && prepareUpdateLockedEditables.has(closestRoot);\n    const hash = (Math.random() + 1).toString(36).substring(7);\n    const options = {\n        allowReenter: true,\n        label: hash,\n        debug: false,\n        ...(args.length && args[args.length - 1] instanceof Object ? args.pop() : {}),\n    };\n    if (options.debug) {\n        console.log(\n            '%cPreparing%c update: ' + options.label +\n            (options.label === hash ? '' : ` (${hash})`) +\n            '%c' + (isPrepareUpdateLocked ? ' LOCKED' : ''),\n            'color: cyan;',\n            'color: white;',\n            'color: red; font-weight: bold;',\n        );\n    }\n    if (isPrepareUpdateLocked) {\n        return () => {\n            if (options.debug) {\n                console.log(\n                    '%cRestoring%c update: ' + options.label +\n                    (options.label === hash ? '' : ` (${hash})`) +\n                    '%c LOCKED',\n                    'color: lightgreen;',\n                    'color: white;',\n                    'color: red; font-weight: bold;',\n                );\n            }\n        };\n    }\n    if (!options.allowReenter && closestRoot) {\n        prepareUpdateLockedEditables.add(closestRoot);\n    }\n    const positions = [...args];\n\n    // Check the state in each direction starting from each position.\n    const restoreData = [];\n    let el, offset;\n    while (positions.length) {\n        // Note: important to get the positions in reverse order to restore\n        // right side before left side.\n        offset = positions.pop();\n        el = positions.pop();\n        const left = getState(el, offset, DIRECTIONS.LEFT);\n        const right = getState(el, offset, DIRECTIONS.RIGHT, left.cType);\n        if (options.debug) {\n            const editable = el && closestElement(el, '.odoo-editor-editable');\n            const oldEditableHTML = editable && makeZeroWidthCharactersVisible(editable.innerHTML).replaceAll(' ', '_') || '';\n            left.oldEditableHTML = oldEditableHTML;\n            right.oldEditableHTML = oldEditableHTML;\n        }\n        restoreData.push(left, right);\n    }\n\n    // Create the callback that will be able to restore the state in each\n    // direction wherever the node in the opposite direction has landed.\n    return function restoreStates() {\n        if (options.debug) {\n            console.log(\n                '%cRestoring%c update: ' + options.label +\n                (options.label === hash ? '' : ` (${hash})`),\n                'color: lightgreen;',\n                'color: white;',\n            );\n        }\n        for (const data of restoreData) {\n            restoreState(data, options.debug);\n        }\n        if (!options.allowReenter && closestRoot) {\n            prepareUpdateLockedEditables.delete(closestRoot);\n        }\n    };\n}\n/**\n * Retrieves the \"state\" from a given position looking at the given direction.\n * The \"state\" is the type of content. The functions also returns the first\n * meaninful node looking in the opposite direction = the first node we trust\n * will not disappear if a command is played in the given direction.\n *\n * Note: only work for in-between nodes positions. If the position is inside a\n * text node, first split it @see splitTextNode.\n *\n * @param {HTMLElement} el\n * @param {number} offset\n * @param {DIRECTIONS} direction @see DIRECTIONS.LEFT @see DIRECTIONS.RIGHT\n * @param {CTYPES} [leftCType]\n * @returns {Object}\n */\nexport function getState(el, offset, direction, leftCType) {\n    const leftDOMPath = leftLeafOnlyNotBlockPath;\n    const rightDOMPath = rightLeafOnlyNotBlockPath;\n\n    let domPath;\n    let inverseDOMPath;\n    const whitespaceAtStartRegex = new RegExp('^' + whitespace + '+');\n    const whitespaceAtEndRegex = new RegExp(whitespace + '+$');\n    const reasons = [];\n    if (direction === DIRECTIONS.LEFT) {\n        domPath = leftDOMPath(el, offset, reasons);\n        inverseDOMPath = rightDOMPath(el, offset);\n    } else {\n        domPath = rightDOMPath(el, offset, reasons);\n        inverseDOMPath = leftDOMPath(el, offset);\n    }\n\n    // TODO I think sometimes, the node we have to consider as the\n    // anchor point to restore the state is not the first one of the inverse\n    // path (like for example, empty text nodes that may disappear\n    // after the command so we would not want to get those ones).\n    const boundaryNode = inverseDOMPath.next().value;\n\n    // We only traverse through deep inline nodes. If we cannot find a\n    // meanfingful state between them, that means we hit a block.\n    let cType = undefined;\n\n    // Traverse the DOM in the given direction to check what type of content\n    // there is.\n    let lastSpace = null;\n    for (const node of domPath) {\n        if (node.nodeType === Node.TEXT_NODE) {\n            // ZWNBSP are technical characters which should be ignored.\n            const value = node.nodeValue.replaceAll('\\ufeff', '');\n            // If we hit a text node, the state depends on the path direction:\n            // any space encountered backwards is a visible space if we hit\n            // visible content afterwards. If going forward, spaces are only\n            // visible if we have content backwards.\n            if (direction === DIRECTIONS.LEFT) {\n                if (!isWhitespace(value)) {\n                    if (lastSpace) {\n                        cType = CTYPES.SPACE;\n                    } else {\n                        const rightLeaf = rightLeafOnlyNotBlockPath(node).next().value;\n                        const hasContentRight = rightLeaf && !whitespaceAtStartRegex.test(rightLeaf.textContent);\n                        cType = !hasContentRight && whitespaceAtEndRegex.test(node.textContent) ? CTYPES.SPACE : CTYPES.CONTENT;\n                    }\n                    break;\n                }\n                if (value.length) {\n                    lastSpace = node;\n                }\n            } else {\n                leftCType = leftCType || getState(el, offset, DIRECTIONS.LEFT).cType;\n                if (whitespaceAtStartRegex.test(value)) {\n                    const leftLeaf = leftLeafOnlyNotBlockPath(node).next().value;\n                    const hasContentLeft = leftLeaf && !whitespaceAtEndRegex.test(leftLeaf.textContent);\n                    const rct = !isWhitespace(value)\n                        ? CTYPES.CONTENT\n                        : getState(...rightPos(node), DIRECTIONS.RIGHT).cType;\n                    cType =\n                        leftCType & CTYPES.CONTENT && rct & (CTYPES.CONTENT | CTYPES.BR) && !hasContentLeft\n                            ? CTYPES.SPACE\n                            : rct;\n                    break;\n                }\n                if (!isWhitespace(value)) {\n                    cType = CTYPES.CONTENT;\n                    break;\n                }\n            }\n        } else if (node.nodeName === 'BR') {\n            cType = CTYPES.BR;\n            break;\n        } else if (isVisible(node)) {\n            // E.g. an image\n            cType = CTYPES.CONTENT;\n            break;\n        }\n    }\n\n    if (cType === undefined) {\n        cType = reasons.includes(PATH_END_REASONS.BLOCK_HIT)\n            ? CTYPES.BLOCK_OUTSIDE\n            : CTYPES.BLOCK_INSIDE;\n    }\n\n    return {\n        node: boundaryNode,\n        direction: direction,\n        cType: cType, // Short for contentType\n    };\n}\nconst priorityRestoreStateRules = [\n    // Each entry is a list of two objects, with each key being optional (the\n    // more key-value pairs, the bigger the priority).\n    // {direction: ..., cType1: ..., cType2: ...}\n    // ->\n    // {spaceVisibility: (false|true), brVisibility: (false|true)}\n    [\n        // Replace a space by &nbsp; when it was not collapsed before and now is\n        // collapsed (one-letter word removal for example).\n        { cType1: CTYPES.CONTENT, cType2: CTYPES.SPACE | CTGROUPS.BLOCK },\n        { spaceVisibility: true },\n    ],\n    [\n        // Replace a space by &nbsp; when it was content before and now it is\n        // a BR.\n        { direction: DIRECTIONS.LEFT, cType1: CTGROUPS.INLINE, cType2: CTGROUPS.BR },\n        { spaceVisibility: true },\n    ],\n    [\n        // Replace a space by &nbsp; when it was content before and now it is\n        // a BR (removal of last character before a BR for example).\n        { direction: DIRECTIONS.RIGHT, cType1: CTGROUPS.CONTENT, cType2: CTGROUPS.BR },\n        { spaceVisibility: true },\n    ],\n    [\n        // Replace a space by &nbsp; when it was visible thanks to a BR which\n        // is now gone.\n        { direction: DIRECTIONS.RIGHT, cType1: CTGROUPS.BR, cType2: CTYPES.SPACE | CTGROUPS.BLOCK },\n        { spaceVisibility: true },\n    ],\n    [\n        // Remove all collapsed spaces when a space is removed.\n        { cType1: CTYPES.SPACE },\n        { spaceVisibility: false },\n    ],\n    [\n        // Remove spaces once the preceeding BR is removed\n        { direction: DIRECTIONS.LEFT, cType1: CTGROUPS.BR },\n        { spaceVisibility: false },\n    ],\n    [\n        // Remove space before block once content is put after it (otherwise it\n        // would become visible).\n        { cType1: CTGROUPS.BLOCK, cType2: CTGROUPS.INLINE | CTGROUPS.BR },\n        { spaceVisibility: false },\n    ],\n    [\n        // Duplicate a BR once the content afterwards disappears\n        { direction: DIRECTIONS.RIGHT, cType1: CTGROUPS.INLINE, cType2: CTGROUPS.BLOCK },\n        { brVisibility: true },\n    ],\n    [\n        // Remove a BR at the end of a block once inline content is put after\n        // it (otherwise it would act as a line break).\n        {\n            direction: DIRECTIONS.RIGHT,\n            cType1: CTGROUPS.BLOCK,\n            cType2: CTGROUPS.INLINE | CTGROUPS.BR,\n        },\n        { brVisibility: false },\n    ],\n    [\n        // Remove a BR once the BR that preceeds it is now replaced by\n        // content (or if it was a BR at the start of a block which now is\n        // a trailing BR).\n        {\n            direction: DIRECTIONS.LEFT,\n            cType1: CTGROUPS.BR | CTGROUPS.BLOCK,\n            cType2: CTGROUPS.INLINE,\n        },\n        { brVisibility: false, extraBRRemovalCondition: brNode => isFakeLineBreak(brNode) },\n    ],\n];\nfunction restoreStateRuleHashCode(direction, cType1, cType2) {\n    return `${direction}-${cType1}-${cType2}`;\n}\nconst allRestoreStateRules = (function () {\n    const map = new Map();\n\n    const keys = ['direction', 'cType1', 'cType2'];\n    for (const direction of Object.values(DIRECTIONS)) {\n        for (const cType1 of Object.values(CTYPES)) {\n            for (const cType2 of Object.values(CTYPES)) {\n                const rule = { direction: direction, cType1: cType1, cType2: cType2 };\n\n                // Search for the rules which match whatever their priority\n                const matchedRules = [];\n                for (const entry of priorityRestoreStateRules) {\n                    let priority = 0;\n                    for (const key of keys) {\n                        const entryKeyValue = entry[0][key];\n                        if (entryKeyValue !== undefined) {\n                            if (\n                                typeof entryKeyValue === 'boolean'\n                                    ? rule[key] === entryKeyValue\n                                    : rule[key] & entryKeyValue\n                            ) {\n                                priority++;\n                            } else {\n                                priority = -1;\n                                break;\n                            }\n                        }\n                    }\n                    if (priority >= 0) {\n                        matchedRules.push([priority, entry[1]]);\n                    }\n                }\n\n                // Create the final rule by merging found rules by order of\n                // priority\n                const finalRule = {};\n                for (let p = 0; p <= keys.length; p++) {\n                    for (const entry of matchedRules) {\n                        if (entry[0] === p) {\n                            Object.assign(finalRule, entry[1]);\n                        }\n                    }\n                }\n\n                // Create an unique identifier for the set of values\n                // direction - state 1 - state2 to add the rule in the map\n                const hashCode = restoreStateRuleHashCode(direction, cType1, cType2);\n                map.set(hashCode, finalRule);\n            }\n        }\n    }\n\n    return map;\n})();\n/**\n * Restores the given state starting before the given while looking in the given\n * direction.\n *\n * @param {Object} prevStateData @see getState\n * @param {boolean} debug=false - if true, adds nicely formatted\n *     console logs to help with debugging.\n * @returns {Object|undefined} the rule that was applied to restore the state,\n *     if any, for testing purposes.\n */\nexport function restoreState(prevStateData, debug=false) {\n    const { node, direction, cType: cType1, oldEditableHTML } = prevStateData;\n    if (!node || !node.parentNode) {\n        // FIXME sometimes we want to restore the state starting from a node\n        // which has been removed by another restoreState call... Not sure if\n        // it is a problem or not, to investigate.\n        return;\n    }\n    const [el, offset] = direction === DIRECTIONS.LEFT ? leftPos(node) : rightPos(node);\n    const { cType: cType2 } = getState(el, offset, direction);\n\n    /**\n     * Knowing the old state data and the new state data, we know if we have to\n     * do something or not, and what to do.\n     */\n    const ruleHashCode = restoreStateRuleHashCode(direction, cType1, cType2);\n    const rule = allRestoreStateRules.get(ruleHashCode);\n    if (debug) {\n        const editable = closestElement(node, '.odoo-editor-editable');\n        console.log(\n            '%c' + makeZeroWidthCharactersVisible(node.textContent).replaceAll(' ', '_') + '\\n' +\n            '%c' + (direction === DIRECTIONS.LEFT ? 'left' : 'right') + '\\n' +\n            '%c' + ctypeToString(cType1) + '\\n' +\n            '%c' + ctypeToString(cType2) + '\\n' +\n            '%c' + 'BEFORE: ' + (oldEditableHTML || '(unavailable)') + '\\n' +\n            '%c' + 'AFTER:  ' + (editable ? makeZeroWidthCharactersVisible(editable.innerHTML).replaceAll(' ', '_') : '(unavailable)') + '\\n',\n            'color: white; display: block; width: 100%;',\n            'color: ' + (direction === DIRECTIONS.LEFT ? 'magenta' : 'lightgreen') + '; display: block; width: 100%;',\n            'color: pink; display: block; width: 100%;',\n            'color: lightblue; display: block; width: 100%;',\n            'color: white; display: block; width: 100%;',\n            'color: white; display: block; width: 100%;',\n            rule,\n        );\n    }\n    if (Object.values(rule).filter(x => x !== undefined).length) {\n        const inverseDirection = direction === DIRECTIONS.LEFT ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n        enforceWhitespace(el, offset, inverseDirection, rule);\n    }\n    return rule;\n}\n/**\n * Enforces the whitespace and BR visibility in the given direction starting\n * from the given position.\n *\n * @param {HTMLElement} el\n * @param {number} offset\n * @param {number} direction @see DIRECTIONS.LEFT @see DIRECTIONS.RIGHT\n * @param {Object} rule\n * @param {boolean} [rule.spaceVisibility]\n * @param {boolean} [rule.brVisibility]\n */\nexport function enforceWhitespace(el, offset, direction, rule) {\n    let domPath, whitespaceAtEdgeRegex;\n    if (direction === DIRECTIONS.LEFT) {\n        domPath = leftLeafOnlyNotBlockPath(el, offset);\n        whitespaceAtEdgeRegex = new RegExp(whitespace + '+$');\n    } else {\n        domPath = rightLeafOnlyNotBlockPath(el, offset);\n        whitespaceAtEdgeRegex = new RegExp('^' + whitespace + '+');\n    }\n\n    const invisibleSpaceTextNodes = [];\n    let foundVisibleSpaceTextNode = null;\n    for (const node of domPath) {\n        if (node.nodeName === 'BR') {\n            if (rule.brVisibility === undefined) {\n                break;\n            }\n            if (rule.brVisibility) {\n                node.before(document.createElement('br'));\n            } else {\n                if (!rule.extraBRRemovalCondition || rule.extraBRRemovalCondition(node)) {\n                    node.remove();\n                }\n            }\n            break;\n        } else if (node.nodeType === Node.TEXT_NODE && !isInPre(node)) {\n            if (whitespaceAtEdgeRegex.test(node.nodeValue)) {\n                // If we hit spaces going in the direction, either they are in a\n                // visible text node and we have to change the visibility of\n                // those spaces, or it is in an invisible text node. In that\n                // last case, we either remove the spaces if there are spaces in\n                // a visible text node going further in the direction or we\n                // change the visiblity or those spaces.\n                if (!isWhitespace(node)) {\n                    foundVisibleSpaceTextNode = node;\n                    break;\n                } else {\n                    invisibleSpaceTextNodes.push(node);\n                }\n            } else if (!isWhitespace(node)) {\n                break;\n            }\n        } else {\n            break;\n        }\n    }\n\n    if (rule.spaceVisibility === undefined) {\n        return;\n    }\n    if (!rule.spaceVisibility) {\n        for (const node of invisibleSpaceTextNodes) {\n            // Empty and not remove to not mess with offset-based positions in\n            // commands implementation, also remove non-block empty parents.\n            node.nodeValue = '';\n            const ancestorPath = closestPath(node.parentNode);\n            let toRemove = null;\n            for (const pNode of ancestorPath) {\n                if (toRemove) {\n                    toRemove.remove();\n                }\n                if (pNode.childNodes.length === 1 && !isBlock(pNode)) {\n                    pNode.after(node);\n                    toRemove = pNode;\n                } else {\n                    break;\n                }\n            }\n        }\n    }\n    const spaceNode = foundVisibleSpaceTextNode || invisibleSpaceTextNodes[0];\n    if (spaceNode) {\n        let spaceVisibility = rule.spaceVisibility;\n        // In case we are asked to replace the space by a &nbsp;, disobey and\n        // do the opposite if that space is currently not visible\n        // TODO I'd like this to not be needed, it feels wrong...\n        if (\n            spaceVisibility &&\n            !foundVisibleSpaceTextNode &&\n            getState(...rightPos(spaceNode), DIRECTIONS.RIGHT).cType & CTGROUPS.BLOCK &&\n            getState(...leftPos(spaceNode), DIRECTIONS.LEFT).cType !== CTYPES.CONTENT\n        ) {\n            spaceVisibility = false;\n        }\n        spaceNode.nodeValue = spaceNode.nodeValue.replace(whitespaceAtEdgeRegex, spaceVisibility ? '\\u00A0' : '');\n    }\n}\n\n/**\n * Takes a color (rgb, rgba or hex) and returns its hex representation. If the\n * color is given in rgba, the background color of the node whose color we're\n * converting is used in conjunction with the alpha to compute the resulting\n * color (using the formula: `alpha*color + (1 - alpha)*background` for each\n * channel).\n *\n * @param {string} rgb\n * @param {HTMLElement} [node]\n * @returns {string} hexadecimal color (#RRGGBB)\n */\nexport function rgbToHex(rgb = '', node = null) {\n    if (rgb.startsWith('#')) {\n        return rgb;\n    } else if (rgb.startsWith('rgba')) {\n        const values = rgb.match(/[\\d\\.]{1,5}/g) || [];\n        const alpha = parseFloat(values.pop());\n        // Retrieve the background color.\n        let bgRgbValues = [];\n        if (node) {\n            let bgColor = getComputedStyle(node).backgroundColor;\n            if (bgColor.startsWith('rgba')) {\n                // The background color is itself rgba so we need to compute\n                // the resulting color using the background color of its\n                // parent.\n                bgColor = rgbToHex(bgColor, node.parentElement);\n            }\n            if (bgColor && bgColor.startsWith('#')) {\n                bgRgbValues = (bgColor.match(/[\\da-f]{2}/gi) || []).map(val => parseInt(val, 16));\n            } else if (bgColor && bgColor.startsWith('rgb')) {\n                bgRgbValues = (bgColor.match(/[\\d\\.]{1,5}/g) || []).map(val => parseInt(val));\n            }\n        }\n        bgRgbValues = bgRgbValues.length ? bgRgbValues : [255, 255, 255]; // Default to white.\n\n        return (\n            '#' +\n            values.map((value, index) => {\n                const converted = Math.floor(alpha * parseInt(value) + (1 - alpha) * bgRgbValues[index]);\n                const hex = parseInt(converted).toString(16);\n                return hex.length === 1 ? '0' + hex : hex;\n            }).join('')\n        );\n    } else {\n        return (\n            '#' +\n            (rgb.match(/\\d{1,3}/g) || [])\n                .map(x => {\n                    x = parseInt(x).toString(16);\n                    return x.length === 1 ? '0' + x : x;\n                })\n                .join('')\n        );\n    }\n}\n\nexport function parseHTML(document, html) {\n    const fragment = document.createDocumentFragment();\n    const parser = new document.defaultView.DOMParser();\n    const parsedDocument = parser.parseFromString(html, 'text/html');\n    fragment.replaceChildren(...parsedDocument.body.childNodes);\n    return fragment;\n}\n\n/**\n * Take a string containing a size in pixels, return that size as a float.\n *\n * @param {string} sizeString\n * @returns {number}\n */\nexport function pxToFloat(sizeString) {\n    return parseFloat(sizeString.replace('px', ''));\n}\n\n/**\n * Returns position of a range in form of object (end\n * position of a range in case of non-collapsed range).\n *\n * @param {HTMLElement} el element for which range postion will be calculated\n * @param {Document} document\n * @param {Object} [options]\n * @param {Number} [options.marginRight] right margin to be considered\n * @param {Number} [options.marginBottom] bottom margin to be considered\n * @param {Number} [options.marginTop] top margin to be considered\n * @param {Number} [options.marginLeft] left margin to be considered\n * @param {Function} [options.getContextFromParentRect] to get context rect from parent\n * @returns {Object | undefined}\n */\nexport function getRangePosition(el, document, options = {}) {\n    const selection = document.getSelection();\n    if (!selection.rangeCount) return;\n    const range = selection.getRangeAt(0);\n    const isRtl = options.direction === 'rtl';\n\n    const marginRight = options.marginRight || 20;\n    const marginBottom = options.marginBottom || 20;\n    const marginTop = options.marginTop || 10;\n    const marginLeft = options.marginLeft || 10;\n\n    let offset;\n    if (range.endOffset - 1 > 0) {\n        const clonedRange = range.cloneRange();\n        clonedRange.setStart(range.endContainer, range.endOffset - 1);\n        clonedRange.setEnd(range.endContainer, range.endOffset);\n        const rect = clonedRange.getBoundingClientRect();\n        offset = { height: rect.height, left: rect.left + rect.width, top: rect.top };\n        clonedRange.detach();\n    }\n\n    if (!offset || offset.height === 0) {\n        const clonedRange = range.cloneRange();\n        const shadowCaret = document.createTextNode('|');\n        clonedRange.insertNode(shadowCaret);\n        clonedRange.selectNode(shadowCaret);\n        const rect = clonedRange.getBoundingClientRect();\n        offset = { height: rect.height, left: rect.left, top: rect.top };\n        shadowCaret.remove();\n        clonedRange.detach();\n    }\n\n    if (isRtl) {\n        // To handle the RTL case we shift the elelement to the left by its size\n        // and handle it the same as left.\n        offset.right = offset.left - el.offsetWidth;\n        const leftMove = Math.max(0, offset.right + el.offsetWidth + marginLeft - window.innerWidth);\n        if (leftMove && offset.right - leftMove > marginRight) {\n            offset.right -= leftMove;\n        } else if (offset.right - leftMove < marginRight) {\n            offset.right = marginRight;\n        }\n    }\n\n    const leftMove = Math.max(0, offset.left + el.offsetWidth + marginRight - window.innerWidth);\n    if (leftMove && offset.left - leftMove > marginLeft) {\n        offset.left -= leftMove;\n    } else if (offset.left - leftMove < marginLeft) {\n        offset.left = marginLeft;\n    }\n\n    if (options.getContextFromParentRect) {\n        const parentContextRect = options.getContextFromParentRect();\n        offset.left += parentContextRect.left;\n        offset.top += parentContextRect.top;\n        if (isRtl) {\n            offset.right += parentContextRect.left;\n        }\n    }\n\n    if (\n        offset.top - marginTop + offset.height + el.offsetHeight > window.innerHeight &&\n        offset.top - el.offsetHeight - marginBottom > 0\n    ) {\n        offset.top -= el.offsetHeight;\n    } else {\n        offset.top += offset.height;\n    }\n\n    if (offset) {\n        offset.top += window.scrollY;\n        offset.left += window.scrollX;\n        if (isRtl) {\n            offset.right += window.scrollX;\n        }\n    }\n    if (isRtl) {\n        // Get the actual right value.\n        offset.right = window.innerWidth - offset.right - el.offsetWidth;\n    }\n\n    return offset;\n}\n\nexport const isNotEditableNode = node =>\n    node.getAttribute &&\n    node.getAttribute('contenteditable') &&\n    node.getAttribute('contenteditable').toLowerCase() === 'false';\n\nexport const leftLeafFirstPath = createDOMPathGenerator(DIRECTIONS.LEFT);\nexport const leftLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.LEFT, {\n    leafOnly: true,\n    stopTraverseFunction: isBlock,\n    stopFunction: isBlock,\n});\nexport const leftLeafOnlyInScopeNotBlockEditablePath = createDOMPathGenerator(DIRECTIONS.LEFT, {\n    leafOnly: true,\n    inScope: true,\n    stopTraverseFunction: node => isNotEditableNode(node) || isBlock(node),\n    stopFunction: node => isNotEditableNode(node) || isBlock(node),\n});\n\nexport const rightLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.RIGHT, {\n    leafOnly: true,\n    stopTraverseFunction: isBlock,\n    stopFunction: isBlock,\n});\n\nexport const rightLeafOnlyPathNotBlockNotEditablePath = createDOMPathGenerator(DIRECTIONS.RIGHT, {\n    leafOnly: true,\n});\nexport const rightLeafOnlyInScopeNotBlockEditablePath = createDOMPathGenerator(DIRECTIONS.RIGHT, {\n    leafOnly: true,\n    inScope: true,\n    stopTraverseFunction: node => isNotEditableNode(node) || isBlock(node),\n    stopFunction: node => isNotEditableNode(node) || isBlock(node),\n});\nexport const rightLeafOnlyNotBlockNotEditablePath = createDOMPathGenerator(DIRECTIONS.RIGHT, {\n    leafOnly: true,\n    stopTraverseFunction: node => isNotEditableNode(node) || isBlock(node),\n    stopFunction: node => isBlock(node) && !isNotEditableNode(node),\n});\n//------------------------------------------------------------------------------\n// Miscelaneous\n//------------------------------------------------------------------------------\nexport function peek(arr) {\n    return arr[arr.length - 1];\n}\n/**\n * Check user OS\n * @returns {boolean}\n */\nexport function isMacOS() {\n    return window.navigator.userAgent.includes('Mac');\n}\n\n/**\n * Remove zero-width spaces from the provided node and its descendants.\n * Note: Does NOT remove zero-width NON-BREAK spaces (feff)!\n *\n * @param {Node} node\n */\nexport function cleanZWS(node) {\n    [node, ...descendants(node)]\n        .filter(node => node.nodeType === Node.TEXT_NODE && node.nodeValue.includes('\\u200B'))\n        .forEach(node => node.nodeValue = node.nodeValue.replace(/\\u200B/g, ''));\n}\n", "/** @odoo-module **/\n\nexport const fonts = {\n    /**\n     * Retrieves all the CSS rules which match the given parser (Regex).\n     *\n     * @param {Regex} filter\n     * @returns {Object[]} Array of CSS rules descriptions (objects). A rule is\n     *          defined by 3 values: 'selector', 'css' and 'names'. 'selector'\n     *          is a string which contains the whole selector, 'css' is a string\n     *          which contains the css properties and 'names' is an array of the\n     *          first captured groups for each selector part. E.g.: if the\n     *          filter is set to match .fa-* rules and capture the icon names,\n     *          the rule:\n     *              '.fa-alias1::before, .fa-alias2::before { hello: world; }'\n     *          will be retrieved as\n     *              {\n     *                  selector: '.fa-alias1::before, .fa-alias2::before',\n     *                  css: 'hello: world;',\n     *                  names: ['.fa-alias1', '.fa-alias2'],\n     *              }\n     */\n    cacheCssSelectors: {},\n    getCssSelectors: function (filter) {\n        if (this.cacheCssSelectors[filter]) {\n            return this.cacheCssSelectors[filter];\n        }\n        this.cacheCssSelectors[filter] = [];\n        var sheets = document.styleSheets;\n        for (var i = 0; i < sheets.length; i++) {\n            var rules;\n            try {\n                // try...catch because Firefox not able to enumerate\n                // document.styleSheets[].cssRules[] for cross-domain\n                // stylesheets.\n                rules = sheets[i].rules || sheets[i].cssRules;\n            } catch {\n                continue;\n            }\n            if (!rules) {\n                continue;\n            }\n\n            for (var r = 0 ; r < rules.length ; r++) {\n                var selectorText = rules[r].selectorText;\n                if (!selectorText) {\n                    continue;\n                }\n                var selectors = selectorText.split(/\\s*,\\s*/);\n                var data = null;\n                for (var s = 0; s < selectors.length; s++) {\n                    var match = selectors[s].trim().match(filter);\n                    if (!match) {\n                        continue;\n                    }\n                    if (!data) {\n                        data = {\n                            selector: match[0],\n                            css: rules[r].cssText.replace(/(^.*\\{\\s*)|(\\s*\\}\\s*$)/g, ''),\n                            names: [match[1]]\n                        };\n                    } else {\n                        data.selector += (', ' + match[0]);\n                        data.names.push(match[1]);\n                    }\n                }\n                if (data) {\n                    this.cacheCssSelectors[filter].push(data);\n                }\n            }\n        }\n        return this.cacheCssSelectors[filter];\n    },\n    /**\n     * List of font icons to load by editor. The icons are displayed in the media\n     * editor and identified like font and image (can be colored, spinned, resized\n     * with fa classes).\n     * To add font, push a new object {base, parser}\n     *\n     * - base: class who appear on all fonts\n     * - parser: regular expression used to select all font in css stylesheets\n     *\n     * @type Array\n     */\n    fontIcons: [{base: 'fa', parser: /\\.(fa-(?:\\w|-)+)::?before/i}],\n    computedFonts: false,\n    /**\n     * Searches the fonts described by the @see fontIcons variable.\n     */\n    computeFonts: function () {\n        if (!this.computedFonts) {\n            var self = this;\n            this.fontIcons.forEach((data) => {\n                data.cssData = self.getCssSelectors(data.parser);\n                data.alias = data.cssData.map((x) => x.names).flat();\n            });\n            this.computedFonts = true;\n        }\n    },\n};\n\nexport default fonts;\n", "/** @odoo-module **/\n\nimport { loadBundle } from \"@web/core/assets\";\nimport { ensureJQuery } from \"@web/core/ensure_jquery\";\nimport { attachComponent } from \"@web_editor/js/core/owl_utils\";\n\nexport async function loadWysiwygFromTextarea(parent, textarea, options) {\n    var loading = textarea.nextElementSibling;\n    if (loading && !loading.classList.contains('o_wysiwyg_loading')) {\n        loading = null;\n    }\n    const $textarea = $(textarea);\n    const currentOptions = Object.assign({}, options);\n    currentOptions.value = currentOptions.value || $textarea.val() || '';\n    if (!currentOptions.value.trim()) {\n        currentOptions.value = '<p><br></p>';\n    }\n\n    await ensureJQuery();\n    await loadBundle(\"web_editor.assets_wysiwyg\");\n    const { Wysiwyg } = await odoo.loader.modules.get('@web_editor/js/wysiwyg/wysiwyg');\n    let wysiwyg;\n    class LegacyWysiwyg extends Wysiwyg {\n        constructor(...args) {\n            super(...args);\n            wysiwyg = this;\n        }\n    }\n\n    const $wysiwygWrapper = $textarea.closest('.o_wysiwyg_textarea_wrapper');\n    const $form = $textarea.closest('form');\n\n    // hide and append the $textarea in $form so it's value will be send\n    // through the form.\n    $textarea.hide();\n    $form.append($textarea);\n    $wysiwygWrapper.html('');\n    const wysiwygWrapper = $wysiwygWrapper[0];\n    await attachComponent(parent, wysiwygWrapper, LegacyWysiwyg, {\n        options: currentOptions,\n        editingValue: currentOptions.value,\n    });\n\n    $form.find('.note-editable').data('wysiwyg', wysiwyg);\n\n    // o_we_selected_image has not always been removed when\n    // saving a post so we need the line below to remove it if it is present.\n    $form.find('.note-editable').find('img.o_we_selected_image').removeClass('o_we_selected_image');\n\n    let b64imagesPending = true;\n    $form.on('click', 'button[type=submit]', (ev) => {\n        if (b64imagesPending) {\n            ev.preventDefault();\n            wysiwyg.savePendingImages().finally(() => {\n                b64imagesPending = false;\n                ev.currentTarget.click();\n            });\n        } else {\n            $form.find('.note-editable').find('img.o_we_selected_image').removeClass('o_we_selected_image');\n            // float-start class messes up the post layout OPW 769721\n            $form.find('.note-editable').find('img.float-start').removeClass('float-start');\n            $textarea.val(wysiwyg.getValue());\n        }\n    });\n\n    return wysiwyg;\n};\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\npublicWidget.registry.SignUpForm = publicWidget.Widget.extend({\n    selector: '.oe_signup_form',\n    events: {\n        'submit': '_onSubmit',\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onSubmit: function () {\n        var $btn = this.$('.oe_login_buttons > button[type=\"submit\"]');\n        $btn.attr('disabled', 'disabled');\n        $btn.prepend('<i class=\"fa fa-refresh fa-spin\"/> ');\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { rpc } from \"@web/core/network/rpc\";\n\npublicWidget.registry.portalDetails = publicWidget.Widget.extend({\n    selector: '.o_portal_details',\n    events: {\n        'change select[name=\"country_id\"]': '_onCountryChange',\n    },\n\n    /**\n     * @override\n     */\n    start: function () {\n        var def = this._super.apply(this, arguments);\n\n        this.$state = this.$('select[name=\"state_id\"]');\n        this.$stateOptions = this.$state.filter(':enabled').find('option:not(:first)');\n        this._adaptAddressForm();\n\n        return def;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _adaptAddressForm: function () {\n        var $country = this.$('select[name=\"country_id\"]');\n        var countryID = ($country.val() || 0);\n        this.$stateOptions.detach();\n        var $displayedState = this.$stateOptions.filter('[data-country_id=' + countryID + ']');\n        var nb = $displayedState.appendTo(this.$state).show().length;\n        this.$state.parent().toggle(nb >= 1);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onCountryChange: function () {\n        this._adaptAddressForm();\n    },\n});\n\nexport const PortalHomeCounters = publicWidget.Widget.extend({\n    selector: '.o_portal_my_home',\n\n    /**\n     * @override\n     */\n    start: function () {\n        var def = this._super.apply(this, arguments);\n        this._updateCounters();\n        return def;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Return a list of counters name linked to a line that we want to keep\n     * regardless of the number of documents present\n     * @private\n     * @returns {Array}\n     */\n    _getCountersAlwaysDisplayed() {\n        return [];\n    },\n\n    /**\n     * @private\n     */\n    async _updateCounters(elem) {\n        const needed = Object.values(this.el.querySelectorAll('[data-placeholder_count]'))\n                                .map(documentsCounterEl => documentsCounterEl.dataset['placeholder_count']);\n        const numberRpc = Math.min(Math.ceil(needed.length / 5), 3); // max 3 rpc, up to 5 counters by rpc ideally\n        const counterByRpc = Math.ceil(needed.length / numberRpc);\n        const countersAlwaysDisplayed = this._getCountersAlwaysDisplayed();\n\n        const proms = [...Array(Math.min(numberRpc, needed.length)).keys()].map(async i => {\n            const documentsCountersData = await rpc(\"/my/counters\", {\n                counters: needed.slice(i * counterByRpc, (i + 1) * counterByRpc)\n            });\n            Object.keys(documentsCountersData).forEach(counterName => {\n                const documentsCounterEl = this.el.querySelector(`[data-placeholder_count='${counterName}']`);\n                documentsCounterEl.textContent = documentsCountersData[counterName];\n                // The element is hidden by default, only show it if its counter is > 0 or if it's in the list of counters always shown\n                if (documentsCountersData[counterName] !== 0 || countersAlwaysDisplayed.includes(counterName)) {\n                    documentsCounterEl.closest('.o_portal_index_card').classList.remove('d-none');\n                }\n            });\n            return documentsCountersData;\n        });\n        return Promise.all(proms).then((results) => {\n            this.el.querySelector('.o_portal_doc_spinner').remove();\n        });\n    },\n});\n\npublicWidget.registry.PortalHomeCounters = PortalHomeCounters;\n\npublicWidget.registry.portalSearchPanel = publicWidget.Widget.extend({\n    selector: '.o_portal_search_panel',\n    events: {\n        'click .dropdown-item': '_onDropdownItemClick',\n        'submit': '_onSubmit',\n    },\n\n    /**\n     * @override\n     */\n    start: function () {\n        var def = this._super.apply(this, arguments);\n        this._adaptSearchLabel(this.$('.dropdown-item.active'));\n        return def;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _adaptSearchLabel: function (elem) {\n        var $label = $(elem).clone();\n        $label.find('span.nolabel').remove();\n        this.$('input[name=\"search\"]').attr('placeholder', $label.text().trim());\n    },\n    /**\n     * @private\n     */\n    _search: function () {\n        var search = new URL(window.location).searchParams;\n        search.set(\"search_in\", this.$('.dropdown-item.active').attr('href')?.replace('#', '') || \"\");\n        search.set(\"search\", this.$('input[name=\"search\"]').val());\n        window.location.search = search.toString();\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onDropdownItemClick: function (ev) {\n        ev.preventDefault();\n        var $item = $(ev.currentTarget);\n        $item.closest('.dropdown-menu').find('.dropdown-item').removeClass('active');\n        $item.addClass('active');\n\n        this._adaptSearchLabel(ev.currentTarget);\n    },\n    /**\n     * @private\n     */\n    _onSubmit: function (ev) {\n        ev.preventDefault();\n        this._search();\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { post } from \"@web/core/network/http_service\";\nimport { Component } from \"@odoo/owl\";\nimport { rpc, RPCError } from \"@web/core/network/rpc\";\n\n/**\n * Widget PortalComposer\n *\n * Display the composer (according to access right)\n *\n */\nvar PortalComposer = publicWidget.Widget.extend({\n    template: 'portal.Composer',\n    events: {\n        'change .o_portal_chatter_file_input': '_onFileInputChange',\n        'click .o_portal_chatter_attachment_btn': '_onAttachmentButtonClick',\n        'click .o_portal_chatter_attachment_delete': 'async _onAttachmentDeleteClick',\n        'click .o_portal_chatter_composer_btn': 'async _onSubmitButtonClick',\n    },\n\n    /**\n     * @constructor\n     */\n    init: function (parent, options) {\n        this._super.apply(this, arguments);\n        this.options = Object.assign({\n            'allow_composer': true,\n            'display_composer': false,\n            'csrf_token': odoo.csrf_token,\n            'token': false,\n            'res_model': false,\n            'res_id': false,\n            'send_button_label': _t(\"Send\"),\n        }, options || {});\n        this.attachments = [];\n        this.notification = this.bindService(\"notification\");\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        var self = this;\n        this.$attachmentButton = this.$('.o_portal_chatter_attachment_btn');\n        this.$fileInput = this.$('.o_portal_chatter_file_input');\n        this.$sendButton = this.$('.o_portal_chatter_composer_btn');\n        this.$attachments = this.$('.o_portal_chatter_composer_input .o_portal_chatter_attachments');\n        this.$inputTextarea = this.$('.o_portal_chatter_composer_input textarea[name=\"message\"]');\n\n        return this._super.apply(this, arguments).then(function () {\n            if (self.options.default_attachment_ids) {\n                self.attachments = self.options.default_attachment_ids || [];\n                self.attachments.forEach((attachment) => {\n                    attachment.state = 'done';\n                });\n                self._updateAttachments();\n            }\n            return Promise.resolve();\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onAttachmentButtonClick: function () {\n        this.$fileInput.click();\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     * @returns {Promise}\n     */\n    _onAttachmentDeleteClick: function (ev) {\n        var self = this;\n        var attachmentId = $(ev.currentTarget).closest('.o_portal_chatter_attachment').data('id');\n        var accessToken = this.attachments.find(attachment => attachment.id === attachmentId).access_token;\n        ev.preventDefault();\n        ev.stopPropagation();\n\n        this.$sendButton.prop('disabled', true);\n\n        return rpc('/portal/attachment/remove', {\n            'attachment_id': attachmentId,\n            'access_token': accessToken,\n        }).then(function () {\n            self.attachments = self.attachments.filter(attachment => attachment.id !== attachmentId);\n            self._updateAttachments();\n            self.$sendButton.prop('disabled', false);\n        });\n    },\n    _prepareAttachmentData: function (file) {\n        return {\n            is_pending: true,\n            thread_id: this.options.res_id,\n            thread_model: this.options.res_model,\n            token: this.options.token,\n            ufile: file,\n        };\n    },\n    /**\n     * @private\n     * @returns {Promise}\n     */\n    _onFileInputChange: function () {\n        var self = this;\n\n        this.$sendButton.prop('disabled', true);\n\n        return Promise.all([...this.$fileInput[0].files].map((file) => {\n            return new Promise(function (resolve, reject) {\n                var data = self._prepareAttachmentData(file);\n                if (odoo.csrf_token) {\n                    data.csrf_token = odoo.csrf_token;\n                }\n                post('/mail/attachment/upload', data).then(function (res) {\n                    let attachment = res.data[\"ir.attachment\"][0]\n                    attachment.state = 'pending';\n                    self.attachments.push(attachment);\n                    self._updateAttachments();\n                    resolve();\n                }).catch(function (error) {\n                    if (error instanceof RPCError) {\n                        self.notification.add(\n                            _t(\"Could not save file <strong>%s</strong>\", escape(file.name)),\n                            { type: 'warning', sticky: true }\n                        );\n                        resolve();\n                    }\n                });\n            });\n        })).then(function () {\n            // ensures any selection triggers a change, even if the same files are selected again\n            self.$fileInput[0].value = null;\n            self.$sendButton.prop('disabled', false);\n        });\n    },\n    /**\n     * prepares data to send message\n     *\n     * @private\n     */\n    _prepareMessageData: function () {\n        return Object.assign(this.options || {}, {\n            thread_model: this.options.res_model,\n            thread_id: this.options.res_id,\n            post_data: {\n                body: this.$('textarea[name=\"message\"]').val(),\n                attachment_ids: this.attachments.map((a) => a.id),\n                message_type: \"comment\",\n                subtype_xmlid: \"mail.mt_comment\",\n            },\n            attachment_tokens: this.attachments.map((a) => a.access_token),\n            token: this.options.token,\n            hash: this.options.hash,\n            pid: this.options.pid,\n        });\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onSubmitButtonClick: function (ev) {\n        ev.preventDefault();\n        const error = this._onSubmitCheckContent();\n        if (error) {\n            this.$inputTextarea.addClass('border-danger');\n            this.$(\".o_portal_chatter_composer_error\").text(error).removeClass('d-none');\n            return Promise.reject();\n        } else {\n            return this._chatterPostMessage(ev.currentTarget.getAttribute('data-action'));\n        }\n    },\n\n    /**\n     * @private\n     */\n    _onSubmitCheckContent: function () {\n        if (!this.$inputTextarea.val().trim() && !this.attachments.length) {\n            return _t('Some fields are required. Please make sure to write a message or attach a document');\n        };\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _updateAttachments: function () {\n        this.$attachments.empty().append(renderToElement('portal.Chatter.Attachments', {\n            attachments: this.attachments,\n            showDelete: true,\n        }));\n    },\n    /**\n     * post message using rpc call and display new message and message count\n     *\n     * @private\n     * @param {String} route\n     * @returns {Promise}\n     */\n    _chatterPostMessage: async function (route) {\n        const result = await rpc(route, this._prepareMessageData());\n        Component.env.bus.trigger('reload_chatter_content', result);\n        return result;\n    },\n});\n\nexport default {\n    PortalComposer: PortalComposer,\n};\n", "/** @odoo-module **/\n\nimport { ConfirmationDialog } from '@web/core/confirmation_dialog/confirmation_dialog';\nimport { renderToMarkup } from \"@web/core/utils/render\";\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { InputConfirmationDialog } from './components/input_confirmation_dialog/input_confirmation_dialog';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\n\npublicWidget.registry.NewAPIKeyButton = publicWidget.Widget.extend({\n    selector: '.o_portal_new_api_key',\n    events: {\n        click: '_onClick'\n    },\n\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n        this.dialog = this.bindService(\"dialog\");\n    },\n\n    async _onClick(e){\n        e.preventDefault();\n        // This call is done just so it asks for the password confirmation before starting displaying the\n        // dialog forms, to mimic the behavior from the backend, in which it asks for the password before\n        // displaying the wizard.\n        // The result of the call is unused. But it's required to call a method with the decorator `@check_identity`\n        // in order to use `handleCheckIdentity`.\n        await handleCheckIdentity(\n            this.orm.call(\"res.users\", \"api_key_wizard\", [user.userId]),\n            this.orm,\n            this.dialog\n        );\n\n        this.call(\"dialog\", \"add\", InputConfirmationDialog, {\n            title: _t(\"New API Key\"),\n            body: renderToMarkup(\"portal.keydescription\"),\n            confirmLabel: _t(\"Confirm\"),\n            confirm: async ({ inputEl }) => {\n                const description = inputEl.value;\n                const wizard_id = await this.orm.create(\"res.users.apikeys.description\", [{ name: description }]);\n                const res = await handleCheckIdentity(\n                    this.orm.call(\"res.users.apikeys.description\", \"make_key\", [wizard_id]),\n                    this.orm,\n                    this.dialog\n                );\n\n                this.call(\"dialog\", \"add\", ConfirmationDialog, {\n                    title: _t(\"API Key Ready\"),\n                    body: renderToMarkup(\"portal.keyshow\", { key: res.context.default_key }),\n                    confirmLabel: _t(\"Close\"),\n                }, {\n                    onClose: () => {\n                        window.location = window.location;\n                    },\n                })\n            }\n        });\n    }\n});\n\npublicWidget.registry.RemoveAPIKeyButton = publicWidget.Widget.extend({\n    selector: '.o_portal_remove_api_key',\n    events: {\n        click: '_onClick'\n    },\n\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n        this.dialog = this.bindService(\"dialog\");\n    },\n\n    async _onClick(e){\n        e.preventDefault();\n        await handleCheckIdentity(\n            this.orm.call(\"res.users.apikeys\", \"remove\", [parseInt(this.el.id)]),\n            this.orm,\n            this.dialog\n        );\n        window.location = window.location;\n    }\n});\n\npublicWidget.registry.portalSecurity = publicWidget.Widget.extend({\n    selector: '.o_portal_security_body',\n\n    /**\n     * @override\n     */\n    init: function () {\n        // Show the \"deactivate your account\" modal if needed\n        $('.modal.show#portal_deactivate_account_modal').removeClass('d-block').modal('show');\n\n        // Remove the error messages when we close the modal,\n        // so when we re-open it again we get a fresh new form\n        $('.modal#portal_deactivate_account_modal').on('hide.bs.modal', (event) => {\n            const $target = $(event.currentTarget);\n            $target.find('.alert').remove();\n            $target.find('.invalid-feedback').remove();\n            $target.find('.is-invalid').removeClass('is-invalid');\n        });\n\n        return this._super(...arguments);\n    },\n\n});\n\n/**\n * Defining what happens when you click the \"Log out from all devices\"\n * on the \"/my/security\" page.\n */\npublicWidget.registry.RevokeSessionsButton = publicWidget.Widget.extend({\n    selector: '#portal_revoke_all_sessions_popup',\n    events: {\n        click: '_onClick',\n    },\n\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n    },\n\n    async _onClick() {\n        const { res_id: checkId } = await this.orm.call(\"res.users\", \"api_key_wizard\", [\n            user.userId,\n        ]);\n        this.call(\"dialog\", \"add\", InputConfirmationDialog, {\n            title: _t(\"Log out from all devices?\"),\n            body: renderToMarkup(\"portal.revoke_all_devices_popup_template\"),\n            confirmLabel: _t(\"Log out from all devices\"),\n            confirm: async ({ inputEl }) => {\n                if (!inputEl.reportValidity()) {\n                    inputEl.classList.add(\"is-invalid\");\n                    return false;\n                }\n\n                await this.orm.write(\"res.users.identitycheck\", [checkId], { password: inputEl.value });\n                try {\n                    await this.orm.call(\n                        \"res.users.identitycheck\",\n                        \"revoke_all_devices\",\n                        [checkId]\n                    );\n                } catch {\n                    inputEl.classList.add(\"is-invalid\");\n                    inputEl.setCustomValidity(_t(\"Check failed\"));\n                    inputEl.reportValidity();\n                    return false;\n                }\n\n                window.location.href = \"/web/session/logout?redirect=/\";\n                return true;\n            },\n            cancel: () => {},\n            onInput: ({ inputEl }) => {\n                inputEl.classList.remove(\"is-invalid\");\n                inputEl.setCustomValidity(\"\");\n            },\n        });\n    },\n});\n\n/**\n * Wraps an RPC call in a check for the result being an identity check action\n * descriptor. If no such result is found, just returns the wrapped promise's\n * result as-is; otherwise shows an identity check dialog and resumes the call\n * on success.\n *\n * Warning: does not in and of itself trigger an identity check, a promise which\n * never triggers and identity check internally will do nothing of use.\n *\n * @param {Promise} wrapped promise to check for an identity check request\n * @param {Function} ormService bound do the widget\n * @param {Function} dialogService dialog service\n * @returns {Promise} result of the original call\n */\nexport async function handleCheckIdentity(wrapped, ormService, dialogService) {\n    return wrapped.then((r) => {\n        if (!(r.type === \"ir.actions.act_window\" && r.res_model === \"res.users.identitycheck\")) {\n            return r;\n        }\n        const checkId = r.res_id;\n        return new Promise((resolve) => {\n            dialogService.add(InputConfirmationDialog, {\n                title: _t(\"Security Control\"),\n                body: renderToMarkup(\"portal.identitycheck\"),\n                confirmLabel: _t(\"Confirm Password\"),\n                confirm: async ({ inputEl }) => {\n                    if (!inputEl.reportValidity()) {\n                        inputEl.classList.add(\"is-invalid\");\n                        return false;\n                    }\n                    let result;\n                    await ormService.write(\"res.users.identitycheck\", [checkId], { password: inputEl.value });\n                    try {\n                        result = await ormService.call(\"res.users.identitycheck\", \"run_check\", [checkId]);\n                    } catch {\n                        inputEl.classList.add(\"is-invalid\");\n                        inputEl.setCustomValidity(_t(\"Check failed\"));\n                        inputEl.reportValidity();\n                        return false;\n                    }\n                    resolve(result);\n                    return true;\n                },\n                cancel: () => {},\n                onInput: ({ inputEl }) => {\n                    inputEl.classList.remove(\"is-invalid\");\n                    inputEl.setCustomValidity(\"\");\n                },\n            });\n        });\n    });\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { deserializeDateTime } from \"@web/core/l10n/dates\";\n\nconst { DateTime } = luxon;\n\nvar PortalSidebar = publicWidget.Widget.extend({\n    /**\n     * @override\n     */\n    start: function () {\n        this._setDelayLabel();\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //---------------------------------------------------------------------------\n\n    /**\n     * Set the due/delay information according to the given date\n     * like : <span class=\"o_portal_sidebar_timeago\" t-att-datetime=\"invoice.date_due\"/>\n     *\n     * @private\n     */\n    _setDelayLabel: function () {\n        var $sidebarTimeago = this.$el.find('.o_portal_sidebar_timeago').toArray();\n        $sidebarTimeago.forEach((el) => {\n            var dateTime = deserializeDateTime($(el).attr('datetime')).startOf('day'),\n                today = DateTime.now().startOf('day'),\n                diff = dateTime.diff(today).as(\"days\"),\n                displayStr;\n\n                if (diff === 0) {\n                    displayStr = _t('Due today');\n                } else if (diff > 0) {\n                    // Workaround: force uniqueness of these two translations. We use %1d because the string\n                    // with %d is already used in mail and mail's translations are not sent to the frontend.\n                    displayStr = _t('Due in %s days', Math.abs(diff).toFixed());\n                } else {\n                    displayStr = _t('%s days overdue', Math.abs(diff).toFixed());\n                }\n                $(el).text(displayStr);\n        });\n    },\n    /**\n     * @private\n     * @param {string} href\n     */\n    _printIframeContent: function (href) {\n        if (!this.printContent) {\n            this.printContent = $('<iframe id=\"print_iframe_content\" src=\"' + href + '\" style=\"display:none\"></iframe>');\n            this.$el.append(this.printContent);\n            this.printContent.on('load', function () {\n                $(this).get(0).contentWindow.print();\n            });\n        } else {\n            this.printContent.get(0).contentWindow.print();\n        }\n    },\n});\nexport default PortalSidebar;\n", "/** @odoo-module */\n\nimport { useEffect } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nexport class InputConfirmationDialog extends ConfirmationDialog {\n    static props = {\n        ...ConfirmationDialog.props,\n        onInput: { type: Function, optional: true },\n    };\n    static template = \"portal.InputConfirmationDialog\";\n\n    setup() {\n        super.setup();\n\n        const onInput = () => {\n            if (this.props.onInput) {\n                this.props.onInput({ inputEl: this.inputEl });\n            }\n        };\n        const onKeydown = (ev) => {\n            if (ev.key && ev.key.toLowerCase() === \"enter\") {\n                ev.preventDefault();\n                this._confirm();\n            }\n        };\n        useEffect(\n            (inputEl) => {\n                this.inputEl = inputEl;\n                if (this.inputEl) {\n                    this.inputEl.focus();\n                    this.inputEl.addEventListener(\"keydown\", onKeydown);\n                    this.inputEl.addEventListener(\"input\", onInput);\n                    return () => {\n                        this.inputEl.removeEventListener(\"keydown\", onKeydown);\n                        this.inputEl.removeEventListener(\"input\", onInput);\n                    };\n                }\n            },\n            () => [this.modalRef.el?.querySelector(\"input\")]\n        );\n    }\n\n    _confirm() {\n        this.execButton(() => this.props.confirm({ inputEl: this.inputEl }));\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, onMounted, useRef, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { addLoadingEffect } from '@web/core/utils/ui';\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { redirect } from \"@web/core/utils/urls\";\nimport { NameAndSignature } from \"@web/core/signature/name_and_signature\";\n\n/**\n * This Component is a signature request form. It uses\n * @see NameAndSignature for the input fields, adds a submit\n * button, and handles the RPC to save the result.\n */\nclass SignatureForm extends Component {\n    static template = \"portal.SignatureForm\"\n    static components = { NameAndSignature }\n    static props = [\"*\"];\n\n    setup() {\n        this.rootRef = useRef(\"root\");\n\n        this.csrfToken = odoo.csrf_token;\n        this.state = useState({\n            error: false,\n            success: false,\n        });\n        this.signature = useState({ name: this.props.defaultName });\n        this.nameAndSignatureProps = {\n            signature: this.signature,\n            fontColor: this.props.fontColor || \"black\",\n        };\n        if (this.props.signatureRatio) {\n            this.nameAndSignatureProps.displaySignatureRatio = this.props.signatureRatio;\n        }\n        if (this.props.signatureType) {\n            this.nameAndSignatureProps.signatureType = this.props.signatureType;\n        }\n        if (this.props.mode) {\n            this.nameAndSignatureProps.mode = this.props.mode;\n        }\n\n        // Correctly set up the signature area if it is inside a modal\n        onMounted(() => {\n            this.rootRef.el.closest('.modal').addEventListener('shown.bs.modal', () => {\n                this.signature.resetSignature();\n            });\n        });\n    }\n\n    get sendLabel() {\n        return this.props.sendLabel || _t(\"Accept & Sign\");\n    }\n\n     /**\n     * Handles click on the submit button.\n     *\n     * This will get the current name and signature and validate them.\n     * If they are valid, they are sent to the server, and the reponse is\n     * handled. If they are invalid, it will display the errors to the user.\n     *\n     * @returns {Promise}\n     */\n    async onClickSubmit() {\n        const button = document.querySelector('.o_portal_sign_submit')\n        const icon = button.removeChild(button.firstChild)\n        const restoreBtnLoading = addLoadingEffect(button);\n\n        const name = this.signature.name;\n        const signature = this.signature.getSignatureImage().split(\",\")[1];\n        const data = await rpc(this.props.callUrl, { name, signature });\n        if (data.force_refresh) {\n            restoreBtnLoading();\n            button.prepend(icon)\n            if (data.redirect_url) {\n                redirect(data.redirect_url);\n            } else {\n                window.location.reload();\n            }\n            // do not resolve if we reload the page\n            return new Promise(() => {});\n        }\n        this.state.error = data.error || false;\n        this.state.success = !data.error && {\n            message: data.message,\n            redirectUrl: data.redirect_url,\n            redirectMessage: data.redirect_message,\n        };\n    }\n}\n\nregistry.category(\"public_components\").add(\"portal.signature_form\", SignatureForm);\n", "import { Deferred } from \"@web/core/utils/concurrency\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { registry } from \"@web/core/registry\";\nimport { memoize } from \"@web/core/utils/functions\";\n\nodoo.portalChatterReady = new Deferred();\n\nconst loader = {\n    loadChatter: memoize(() => loadBundle(\"portal.assets_chatter\")),\n};\nexport const portalChatterBootService = {\n    start() {\n        const chatterEl = document.querySelector(\".o_portal_chatter\");\n        if (chatterEl) {\n            loader.loadChatter();\n        }\n    },\n};\nregistry.category(\"services\").add(\"portal.chatter.boot\", portalChatterBootService);\n", "/** @odoo-module **/\n\nimport { scrollTo } from \"@web/core/utils/scrolling\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport PortalSidebar from \"@portal/js/portal_sidebar\";\n\npublicWidget.registry.AccountPortalSidebar = PortalSidebar.extend({\n    selector: '.o_portal_invoice_sidebar',\n    events: {\n        'click .o_portal_invoice_print': '_onPrintInvoice',\n    },\n\n    /**\n     * @override\n     */\n    start: function () {\n        var def = this._super.apply(this, arguments);\n\n        var $invoiceHtml = this.$el.find('iframe#invoice_html');\n        var updateIframeSize = this._updateIframeSize.bind(this, $invoiceHtml);\n\n        $(window).on('resize', updateIframeSize);\n\n        var iframeDoc = $invoiceHtml[0].contentDocument || $invoiceHtml[0].contentWindow.document;\n        if (iframeDoc.readyState === 'complete') {\n            updateIframeSize();\n        } else {\n            $invoiceHtml.on('load', updateIframeSize);\n        }\n\n        return def;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when the iframe is loaded or the window is resized on customer portal.\n     * The goal is to expand the iframe height to display the full report without scrollbar.\n     *\n     * @private\n     * @param {object} $el: the iframe\n     */\n    _updateIframeSize: function ($el) {\n        var $wrapwrap = $el.contents().find('div#wrapwrap');\n        // Set it to 0 first to handle the case where scrollHeight is too big for its content.\n        $el.height(0);\n        $el.height($wrapwrap[0].scrollHeight);\n\n        // scroll to the right place after iframe resize\n        const isAnchor = /^#[\\w-]+$/.test(window.location.hash)\n        if (!isAnchor) {\n            return;\n        }\n        var $target = $(window.location.hash);\n        if (!$target.length) {\n            return;\n        }\n        scrollTo($target[0], { behavior: \"instant\" });\n    },\n    /**\n     * @private\n     * @param {MouseEvent} ev\n     */\n    _onPrintInvoice: function (ev) {\n        ev.preventDefault();\n        var href = $(ev.currentTarget).attr('href');\n        this._printIframeContent(href);\n    },\n});\n", "/** @odoo-module */\n\nimport { PortalHomeCounters } from '@portal/js/portal';\n\nPortalHomeCounters.include({\n    /**\n     * @override\n     */\n    _getCountersAlwaysDisplayed() {\n        return this._super(...arguments).concat(['invoice_count']);\n    },\n});\n", "/** @odoo-module **/\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\n\nimport { accountTaxHelpers } from \"@account/helpers/account_tax\";\n\nimport { xml, useState, Component } from \"@odoo/owl\";\n\nexport class TestsSharedJsPython extends Component {\n    static template = xml`\n        <button t-attf-class=\"#{state.done ? 'text-success' : ''}\" t-on-click=\"processTests\">Test</button>\n    `;\n    static props = {\n        tests: { type: Array, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({ done: false });\n    }\n\n    processTest(params) {\n        if (params.test === \"taxes_computation\") {\n            const kwargs = {\n                product: params.product,\n                precision_rounding: params.precision_rounding,\n                rounding_method: params.rounding_method,\n            };\n            const results = {\n                results: accountTaxHelpers.get_tax_details(\n                    params.taxes,\n                    params.price_unit,\n                    params.quantity,\n                    kwargs,\n                )\n            };\n            if (params.rounding_method === \"round_globally\") {\n                results.total_excluded_results = accountTaxHelpers.get_tax_details(\n                    params.taxes,\n                    results.results.total_excluded / params.quantity,\n                    params.quantity,\n                    {...kwargs, special_mode: \"total_excluded\"}\n                );\n                results.total_included_results = accountTaxHelpers.get_tax_details(\n                    params.taxes,\n                    results.results.total_included / params.quantity,\n                    params.quantity,\n                    {...kwargs, special_mode: \"total_included\"}\n                );\n            }\n            return results;\n        }\n        if (params.test === \"adapt_price_unit_to_another_taxes\") {\n            return {\n                price_unit: accountTaxHelpers.adapt_price_unit_to_another_taxes(\n                    params.price_unit,\n                    params.product,\n                    params.original_taxes,\n                    params.new_taxes\n                )\n            }\n        }\n        if (params.test === \"tax_totals_summary\") {\n            const document = this.populateDocument(params.document);\n            const taxTotals = accountTaxHelpers.get_tax_totals_summary(\n                document.lines,\n                document.currency,\n                document.company,\n                {cash_rounding: document.cash_rounding}\n            );\n            return {tax_totals: taxTotals};\n        }\n        if (params.test === \"tax_total\") {\n            const document = this.populateDocument(params.document);\n            const taxTotals = accountTaxHelpers.get_tax_totals_summary(\n                document.lines,\n                document.currency,\n                document.company,\n                {cash_rounding: document.cash_rounding}\n            );\n            return {tax_amount_currency: taxTotals.tax_amount_currency};\n        }\n    }\n\n    async processTests() {\n        const tests = this.props.tests || [];\n        const results = tests.map(this.processTest.bind(this));\n        await rpc(\"/account/post_tests_shared_js_python\", { results: results });\n        this.state.done = true;\n    }\n\n    populateDocument(document) {\n        const base_lines = document.lines.map(line => accountTaxHelpers.prepare_base_line_for_taxes_computation(null, line));\n        accountTaxHelpers.add_tax_details_in_base_lines(base_lines, document.company);\n        accountTaxHelpers.round_base_lines_tax_details(base_lines, document.company);\n        return {\n            ...document,\n            lines: base_lines,\n        }\n    }\n}\n\nregistry.category(\"public_components\").add(\"account.tests_shared_js_python\", TestsSharedJsPython);\n", "import { floatIsZero, roundPrecision } from \"@web/core/utils/numbers\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport const accountTaxHelpers = {\n    // -------------------------------------------------------------------------\n    // HELPERS IN BOTH PYTHON/JAVASCRIPT (account_tax.js / account_tax.py)\n\n    // PREPARE TAXES COMPUTATION\n    // -------------------------------------------------------------------------\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    eval_taxes_computation_prepare_product_values(default_product_values, product) {\n        const product_values = {};\n        for (const [field_name, field_info] of Object.entries(default_product_values)) {\n            product_values[field_name] = product\n                ? product[field_name] || field_info.default_value\n                : field_info.default_value;\n        }\n        return product_values;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    batch_for_taxes_computation(taxes, { special_mode = false } = {}) {\n        function sort_key(taxes) {\n            return taxes.sort((t1, t2) => t1.sequence - t2.sequence || t1.id - t2.id);\n        }\n\n        const results = {\n            batch_per_tax: {},\n            group_per_tax: {},\n            sorted_taxes: [],\n        };\n\n        // Flatten the taxes.\n        for (const tax of sort_key(taxes)) {\n            if (tax.amount_type === \"group\") {\n                const children = sort_key(tax.children_tax_ids);\n                for (const child of children) {\n                    results.group_per_tax[child.id] = tax;\n                    results.sorted_taxes.push(child);\n                }\n            } else {\n                results.sorted_taxes.push(tax);\n            }\n        }\n\n        // Group them per batch.\n        let batch = [];\n        let is_base_affected = false;\n        for (const tax of results.sorted_taxes.toReversed()) {\n            if (batch.length > 0) {\n                const same_batch =\n                    tax.amount_type === batch[0].amount_type &&\n                    (special_mode || tax.price_include === batch[0].price_include) &&\n                    tax.include_base_amount === batch[0].include_base_amount &&\n                    ((tax.include_base_amount && !is_base_affected) || !tax.include_base_amount);\n                if (!same_batch) {\n                    for (const batch_tax of batch) {\n                        results.batch_per_tax[batch_tax.id] = batch;\n                    }\n                    batch = [];\n                }\n            }\n\n            is_base_affected = tax.is_base_affected;\n            batch.push(tax);\n        }\n\n        if (batch.length !== 0) {\n            for (const batch_tax of batch) {\n                results.batch_per_tax[batch_tax.id] = batch;\n            }\n        }\n        return results;\n    },\n\n    propagate_extra_taxes_base(taxes, tax, taxes_data, { special_mode = false } = {}) {\n        function* get_tax_before() {\n            for (const tax_before of taxes) {\n                if (taxes_data[tax.id].batch.includes(tax_before)) {\n                    break;\n                }\n                yield tax_before;\n            }\n        }\n\n        function* get_tax_after() {\n            for (const tax_after of taxes.toReversed()) {\n                if (taxes_data[tax.id].batch.includes(tax_after)) {\n                    break;\n                }\n                yield tax_after;\n            }\n        }\n\n        function add_extra_base(other_tax, sign) {\n            const tax_amount = taxes_data[tax.id].tax_amount;\n            if (!(\"tax_amount\" in taxes_data[other_tax.id])) {\n                taxes_data[other_tax.id].extra_base_for_tax += sign * tax_amount;\n            }\n            taxes_data[other_tax.id].extra_base_for_base += sign * tax_amount;\n        }\n\n        if (tax.price_include) {\n            // Case: special mode is False or 'total_included'\n            if (!special_mode || special_mode === \"total_included\") {\n                if (!tax.include_base_amount) {\n                    for (const other_tax of get_tax_after()) {\n                        if (other_tax.price_include) {\n                            add_extra_base(other_tax, -1)\n                        }\n                    }\n                }\n                for (const other_tax of get_tax_before()) {\n                    add_extra_base(other_tax, -1);\n                }\n\n            // Case: special_mode = 'total_excluded'\n            } else {\n                for (const other_tax of get_tax_after()) {\n                    if (!other_tax.price_include || tax.include_base_amount) {\n                        add_extra_base(other_tax, 1);\n                    }\n                }\n            }\n\n        } else if (!tax.price_include) {\n            // Case: special_mode is False or 'total_excluded'\n            if (!special_mode || special_mode === \"total_excluded\") {\n                if (tax.include_base_amount) {\n                    for (const other_tax of get_tax_after()) {\n                        add_extra_base(other_tax, 1);\n                    }\n                }\n\n            // Case: special_mode = 'total_included'\n            } else {\n                if (!tax.include_base_amount) {\n                    for (const other_tax of get_tax_after()) {\n                        add_extra_base(other_tax, -1);\n                    }\n                }\n                for (const other_tax of get_tax_before()) {\n                    add_extra_base(other_tax, -1);\n                }\n            }\n        }\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    eval_tax_amount_fixed_amount(tax, batch, raw_base, evaluation_context) {\n        if (tax.amount_type === \"fixed\") {\n            return evaluation_context.quantity * tax.amount;\n        }\n        return null;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    eval_tax_amount_price_included(tax, batch, raw_base, evaluation_context) {\n        if (tax.amount_type === \"percent\") {\n            const total_percentage =\n                batch.reduce(\n                    (sum, batch_tax) => sum + batch_tax.amount,\n                    0\n                ) / 100.0;\n            const to_price_excluded_factor =\n                total_percentage !== -1 ? 1 / (1 + total_percentage) : 0.0;\n            return (raw_base * to_price_excluded_factor * tax.amount) / 100.0;\n        }\n\n        if (tax.amount_type === \"division\") {\n            return (raw_base * tax.amount) / 100.0;\n        }\n        return null;\n    },\n\n    /**\n     * [!] Mirror of the same method in account_tax.py.\n     * PLZ KEEP BOTH METHODS CONSISTENT WITH EACH OTHERS.\n     */\n    eval_tax_amount_price_excluded(tax, batch, raw_base, evaluation_context) {\n        if (tax.amount_type === \"percent\") {\n            return (raw_base * tax.amount) / 100.0;\n        }\n\n        if (tax.amount_type === \"division\") {\n            const total_percentage =\n                batch.reduce(\n                    (sum, batch_tax) => sum + batch_tax.amount,\n                    0\n                ) / 100.0;\n            const incl_base_multiplicator = total_percentage === 1.0 ? 1.0 : 1 - total_percentage;\n            return (raw_base * tax.amount) / 100.0 / incl_base_multiplicator;\n        }\n        return null;\n    },\n\n    eval_raw_base(quantity, price_unit, evaluation_context) {\n        return quantity * price_unit;\n    },\n\n    get_tax_details(\n        taxes,\n        price_unit,\n        quantity,\n        {\n            precision_rounding = null,\n            rounding_method = \"round_per_line\",\n            // When product is null, we need the product default values to make the \"formula\" taxes\n            // working. In that case, we need to deal with the product default values before calling this\n            // method because we have no way to deal with it automatically in this method since it depends of\n            // the type of involved fields and we don't have access to this information js-side.\n            product = null,\n            special_mode = false,\n        } = {}\n    ) {\n        const self = this;\n\n        function add_tax_amount_to_results(tax, tax_amount) {\n            taxes_data[tax.id].tax_amount = tax_amount;\n            if (rounding_method === \"round_per_line\") {\n                taxes_data[tax.id].tax_amount = roundPrecision(\n                    taxes_data[tax.id].tax_amount,\n                    precision_rounding\n                );\n            }\n            if (tax.has_negative_factor){\n                reverse_charge_taxes_data[tax.id].tax_amount = -taxes_data[tax.id].tax_amount;\n            }\n\n            self.propagate_extra_taxes_base(sorted_taxes, tax, taxes_data, {\n                special_mode: special_mode,\n            });\n        }\n\n        function eval_tax_amount(tax_amount_function, tax) {\n            const is_already_computed = \"tax_amount\" in taxes_data[tax.id];\n            if (is_already_computed) {\n                return;\n            }\n\n            const tax_amount = tax_amount_function(\n                tax,\n                taxes_data[tax.id].batch,\n                raw_base + taxes_data[tax.id].extra_base_for_tax,\n                evaluation_context\n            );\n            if (tax_amount !== null) {\n                add_tax_amount_to_results(tax, tax_amount);\n            }\n        }\n\n        // Flatten the taxes and order them.\n\n        function prepare_tax_extra_data(tax, kwargs = {}) {\n            let price_include;\n            if (special_mode === \"total_included\") {\n                price_include = true;\n            } else if (special_mode === \"total_excluded\") {\n                price_include = false;\n            } else {\n                price_include = tax.price_include;\n            }\n            return {\n                ...kwargs,\n                tax: tax,\n                price_include: price_include,\n                extra_base_for_tax: 0.0,\n                extra_base_for_base: 0.0,\n            };\n        }\n\n        const batching_results = this.batch_for_taxes_computation(taxes, {\n            special_mode: special_mode,\n        });\n        const sorted_taxes = batching_results.sorted_taxes;\n        const taxes_data = {};\n        const reverse_charge_taxes_data = {};\n        for (const tax of sorted_taxes) {\n            taxes_data[tax.id] = prepare_tax_extra_data(tax, {\n                group: batching_results.group_per_tax[tax.id],\n                batch: batching_results.batch_per_tax[tax.id],\n            });\n            if (tax.has_negative_factor) {\n                reverse_charge_taxes_data[tax.id] = {\n                    ...taxes_data[tax.id],\n                    is_reverse_charge: true,\n                }\n            }\n        }\n\n        const raw_base_evaluation_context = {\n            taxes: sorted_taxes,\n            precision_rounding: precision_rounding,\n        };\n        let raw_base = this.eval_raw_base(quantity, price_unit, raw_base_evaluation_context);\n        if (rounding_method === \"round_per_line\") {\n            raw_base = roundPrecision(raw_base, precision_rounding);\n        }\n\n        let evaluation_context = {\n            product: product || {},\n            price_unit: price_unit,\n            quantity: quantity,\n            raw_base: raw_base,\n            special_mode: special_mode,\n            precision_rounding: precision_rounding,\n        };\n\n        // Define the order in which the taxes must be evaluated.\n        // Fixed taxes are computed directly because they could affect the base of a price included batch right after.\n        for (const tax of sorted_taxes.toReversed()) {\n            eval_tax_amount(this.eval_tax_amount_fixed_amount.bind(this), tax);\n        }\n\n        // Then, let's travel the batches in the reverse order and process the price-included taxes.\n        for (const tax of sorted_taxes.toReversed()) {\n            if (taxes_data[tax.id].price_include) {\n                eval_tax_amount(this.eval_tax_amount_price_included.bind(this), tax);\n            }\n        }\n\n        // Then, let's travel the batches in the normal order and process the price-excluded taxes.\n        for (const tax of sorted_taxes) {\n            if (!taxes_data[tax.id].price_include) {\n                eval_tax_amount(this.eval_tax_amount_price_excluded.bind(this), tax);\n            }\n        }\n\n        // Mark the base to be computed in the descending order. The order doesn't matter for no special mode or 'total_excluded' but\n        // it must be in the reverse order when special_mode is 'total_included'.\n        for (const tax of sorted_taxes.toReversed()) {\n            if (!(\"tax_amount\" in taxes_data[tax.id])) {\n                continue;\n            }\n\n            const total_tax_amount = taxes_data[tax.id].batch.reduce(\n                (sum, other_tax) => sum + taxes_data[other_tax.id].tax_amount,\n                0\n            );\n            let base = raw_base + taxes_data[tax.id].extra_base_for_base;\n            if (\n                taxes_data[tax.id].price_include &&\n                (!special_mode || special_mode === \"total_included\")\n            ) {\n                base -= total_tax_amount;\n            }\n            taxes_data[tax.id].base = base;\n            if(tax.has_negative_factor){\n                reverse_charge_taxes_data[tax.id].base = base;\n            }\n        }\n\n        const taxes_data_list = [];\n        for (const tax of sorted_taxes) {\n            const tax_data = taxes_data[tax.id];\n            if (\"tax_amount\" in tax_data){\n                taxes_data_list.push(tax_data);\n                if (tax.has_negative_factor) {\n                    taxes_data_list.push(reverse_charge_taxes_data[tax.id]);\n                }\n            }\n        }\n\n        let total_excluded, total_included;\n        if (taxes_data_list.length > 0) {\n            total_excluded = taxes_data_list[0].base;\n            const tax_amount = taxes_data_list.reduce(\n                (sum, tax_data) => sum + tax_data.tax_amount,\n                0\n            );\n            total_included = total_excluded + tax_amount;\n        } else {\n            total_excluded = total_included = raw_base;\n        }\n\n        return {\n            total_excluded: total_excluded,\n            total_included: total_included,\n            taxes_data: taxes_data_list.map(tax_data => Object.assign({}, {\n                tax: tax_data.tax,\n                group: batching_results.group_per_tax[tax_data.tax.id],\n                batch: batching_results.batch_per_tax[tax_data.tax.id],\n                tax_amount: tax_data.tax_amount,\n                base_amount: tax_data.base,\n                is_reverse_charge: tax_data.is_reverse_charge || false\n            })),\n        };\n    },\n\n    // -------------------------------------------------------------------------\n    // MAPPING PRICE_UNIT\n    // -------------------------------------------------------------------------\n\n    adapt_price_unit_to_another_taxes(price_unit, product, original_taxes, new_taxes) {\n        const original_tax_ids = new Set(original_taxes.map((x) => x.id));\n        const new_tax_ids = new Set(new_taxes.map((x) => x.id));\n        if (\n            (original_tax_ids.size === new_tax_ids.size &&\n                [...original_tax_ids].every((value) => new_tax_ids.has(value))) ||\n            original_taxes.some((x) => !x.price_include)\n        ) {\n            return price_unit;\n        }\n\n        // Find the price unit without tax.\n        let taxes_computation = this.get_tax_details(original_taxes, price_unit, 1.0, {\n            rounding_method: \"round_globally\",\n            product: product,\n        });\n        price_unit = taxes_computation.total_excluded;\n\n        // Find the new price unit after applying the price included taxes.\n        taxes_computation = this.get_tax_details(new_taxes, price_unit, 1.0, {\n            rounding_method: \"round_globally\",\n            product: product,\n            special_mode: \"total_excluded\",\n        });\n        let delta = 0.0;\n        for (const tax_data of taxes_computation.taxes_data) {\n            if (tax_data.tax.price_include) {\n                delta += tax_data.tax_amount;\n            }\n        }\n        return price_unit + delta;\n    },\n\n    // -------------------------------------------------------------------------\n    // GENERIC REPRESENTATION OF BUSINESS OBJECTS & METHODS\n    // -------------------------------------------------------------------------\n\n    get_base_line_field_value_from_record(record, field, extra_values, fallback) {\n        if (field in extra_values) {\n            return extra_values[field] || fallback;\n        }\n        if (field in record) {\n            return record[field] || fallback;\n        }\n        return fallback;\n    },\n\n    prepare_base_line_for_taxes_computation(record, kwargs){\n        const load = (field, fallback) => this.get_base_line_field_value_from_record(record, field, kwargs, fallback);\n\n        return {\n            ...kwargs,\n            record: record,\n            id: load('id', 0),\n            product_id: load('product_id', {}),\n            tax_ids: load('tax_ids', {}),\n            price_unit: load('price_unit', 0.0),\n            quantity: load('quantity', 0.0),\n            discount: load('discount', 0.0),\n            currency_id: load('currency_id', {}),\n            sign: load('sign', 1.0),\n            special_mode: kwargs.special_mode || false,\n            special_type: kwargs.special_type || false,\n        }\n    },\n\n    add_tax_details_in_base_line(base_line, company) {\n        const price_unit_after_discount = base_line.price_unit * (1 - (base_line.discount / 100.0));\n        const currency_pd = base_line.currency_id.rounding;\n        const company_currency_pd = company.currency_id.rounding;\n        const taxes_computation = this.get_tax_details(\n            base_line.tax_ids,\n            price_unit_after_discount,\n            base_line.quantity,\n            {\n                precision_rounding: currency_pd,\n                rounding_method: company.tax_calculation_rounding_method,\n                product: base_line.product_id,\n                special_mode: base_line.special_mode\n            }\n        );\n\n        const rate = base_line.rate;\n        const tax_details = base_line.tax_details = {\n            total_excluded_currency: taxes_computation.total_excluded,\n            total_excluded: rate ? taxes_computation.total_excluded / rate : 0.0,\n            total_included_currency: taxes_computation.total_included,\n            total_included: rate ? taxes_computation.total_included / rate : 0.0,\n            taxes_data: []\n        };\n\n        if (company.tax_calculation_rounding_method === 'round_per_line') {\n            tax_details.total_excluded = roundPrecision(tax_details.total_excluded, currency_pd);\n            tax_details.total_included = roundPrecision(tax_details.total_included, currency_pd);\n        }\n\n        for (const tax_data of taxes_computation.taxes_data) {\n            let tax_amount = rate ? tax_data.tax_amount / rate : 0.0;\n            let base_amount = rate ? tax_data.base_amount / rate : 0.0;\n\n            if (company.tax_calculation_rounding_method === 'round_per_line') {\n                tax_amount = roundPrecision(tax_amount, company_currency_pd);\n                base_amount = roundPrecision(base_amount, company_currency_pd);\n            }\n\n            tax_details.taxes_data.push({\n                ...tax_data,\n                tax_amount_currency: tax_data.tax_amount,\n                tax_amount: tax_amount,\n                base_amount_currency: tax_data.base_amount,\n                base_amount: base_amount\n            });\n        }\n    },\n\n    add_tax_details_in_base_lines(base_lines, company) {\n        for(const base_line of base_lines){\n            this.add_tax_details_in_base_line(base_line, company);\n        }\n    },\n\n    round_base_lines_tax_details(base_lines, company) {\n        const company_pd = company.currency_id.rounding;\n        const total_per_tax = {};\n        for (const base_line of base_lines) {\n            const currency = base_line.currency_id;\n            const currency_pd = currency.rounding;\n            const tax_details = base_line.tax_details;\n            tax_details.delta_base_amount_currency = 0.0;\n            tax_details.delta_base_amount = 0.0;\n            tax_details.raw_total_excluded_currency = tax_details.total_excluded_currency;\n            tax_details.total_excluded_currency = roundPrecision(tax_details.total_excluded_currency, currency_pd);\n            tax_details.raw_total_excluded = tax_details.total_excluded;\n            tax_details.total_excluded = roundPrecision(tax_details.total_excluded, company_pd);\n            tax_details.raw_total_included_currency = tax_details.total_included_currency;\n            tax_details.total_included_currency = roundPrecision(tax_details.total_included_currency, currency_pd);\n            tax_details.raw_total_included = tax_details.total_included;\n            tax_details.total_included = roundPrecision(tax_details.total_included, company_pd);\n\n            for (const tax_data of tax_details.taxes_data) {\n                const tax = tax_data.tax;\n\n                tax_data.raw_tax_amount_currency = tax_data.tax_amount_currency;\n                tax_data.tax_amount_currency = roundPrecision(tax_data.tax_amount_currency, currency_pd);\n                tax_data.raw_tax_amount = tax_data.tax_amount;\n                tax_data.tax_amount = roundPrecision(tax_data.tax_amount, company_pd);\n                tax_data.raw_base_amount_currency = tax_data.base_amount_currency;\n                tax_data.base_amount_currency = roundPrecision(tax_data.base_amount_currency, currency_pd);\n                tax_data.raw_base_amount = tax_data.base_amount;\n                tax_data.base_amount = roundPrecision(tax_data.base_amount, company_pd);\n\n                const key = [tax.id, currency.id];\n                if (!(key in total_per_tax)) {\n                    total_per_tax[key] = {\n                        tax: tax,\n                        currency_pd: currency.rounding,\n                        company_currency_pd: company.currency_id.rounding,\n                        base_amount_currency: 0.0,\n                        base_amount: 0.0,\n                        raw_base_amount_currency: 0.0,\n                        raw_base_amount: 0.0,\n                        tax_amount_currency: 0.0,\n                        tax_amount: 0.0,\n                        raw_tax_amount_currency: 0.0,\n                        raw_tax_amount: 0.0,\n                        base_lines: []\n                    };\n                }\n\n                const amounts = total_per_tax[key];\n                amounts.tax_amount_currency += tax_data.tax_amount_currency;\n                amounts.raw_tax_amount_currency += tax_data.raw_tax_amount_currency;\n                amounts.tax_amount += tax_data.tax_amount;\n                amounts.raw_tax_amount += tax_data.raw_tax_amount;\n                amounts.base_amount_currency += tax_data.base_amount_currency;\n                amounts.raw_base_amount_currency += tax_data.raw_base_amount_currency;\n                amounts.base_amount += tax_data.base_amount;\n                amounts.raw_base_amount += tax_data.raw_base_amount;\n                if (!base_line.special_type) {\n                    amounts.base_lines.push(base_line);\n                }\n            }\n        }\n\n        // Round 'total_per_tax'.\n        for (const amounts of Object.values(total_per_tax)) {\n            amounts.raw_tax_amount_currency = roundPrecision(amounts.raw_tax_amount_currency, amounts.currency_pd);\n            amounts.raw_tax_amount = roundPrecision(amounts.raw_tax_amount, amounts.company_currency_pd);\n            amounts.raw_base_amount_currency = roundPrecision(amounts.raw_base_amount_currency, amounts.currency_pd);\n            amounts.raw_base_amount = roundPrecision(amounts.raw_base_amount, amounts.company_currency_pd);\n        }\n\n        // Dispatch the delta across the base lines.\n        for (const amounts of Object.values(total_per_tax)) {\n            if (!amounts.base_lines.length){\n                continue;\n            }\n\n            const base_line = amounts.base_lines.sort(\n                (a, b) => a.tax_details.total_included_currency - b.tax_details.total_included_currency\n            )[0];\n\n            const tax_details = base_line.tax_details;\n            const [index, tax_data] = tax_details.taxes_data.map((x, i) => [i, x]).find(([i, x]) => x.tax.id === amounts.tax.id);\n\n            const delta_base_amount_currency = amounts.raw_base_amount_currency - amounts.base_amount_currency;\n            const delta_base_amount = amounts.raw_base_amount - amounts.base_amount;\n\n            if (index === 0) {\n                tax_details.delta_base_amount_currency += delta_base_amount_currency;\n                tax_details.delta_base_amount += delta_base_amount;\n            }\n\n            tax_data.base_amount_currency += delta_base_amount_currency;\n            tax_data.base_amount += delta_base_amount;\n            tax_data.tax_amount_currency += amounts.raw_tax_amount_currency - amounts.tax_amount_currency;\n            tax_data.tax_amount += amounts.raw_tax_amount - amounts.tax_amount;\n        }\n    },\n\n    // -------------------------------------------------------------------------\n    // TAX TOTALS SUMMARY\n    // -------------------------------------------------------------------------\n\n    get_tax_totals_summary(base_lines, currency, company, {cash_rounding = null} = {}) {\n        const company_pd = company.currency_id.rounding;\n        const tax_totals_summary = {\n            currency_id: currency.id,\n            currency_pd: currency.rounding,\n            company_currency_id: company.currency_id.id,\n            company_currency_pd: company.currency_id.rounding,\n            has_tax_groups: false,\n            subtotals: [],\n            base_amount_currency: 0.0,\n            base_amount: 0.0,\n            tax_amount_currency: 0.0,\n            tax_amount: 0.0,\n        };\n\n        // Global tax values.\n        const global_grouping_function = (base_line, tax_data) => true;\n\n        let base_lines_aggregated_values = this.aggregate_base_lines_tax_details(base_lines, global_grouping_function);\n        let values_per_grouping_key = this.aggregate_base_lines_aggregated_values(base_lines_aggregated_values);\n\n        for (const values of Object.values(values_per_grouping_key)) {\n            if (values.grouping_key) {\n                tax_totals_summary.has_tax_groups = true;\n            }\n            for (const key of ['base_amount_currency', 'base_amount', 'tax_amount_currency', 'tax_amount']) {\n                tax_totals_summary[key] += values[key];\n            }\n        }\n\n        // Subtotals.\n        const untaxed_amount_subtotal_label = _t(\"Untaxed Amount\");\n        const subtotals = {};\n\n        const subtotal_grouping_function = (base_line, tax_data) =>\n            tax_data.tax.tax_group_id.preceding_subtotal || untaxed_amount_subtotal_label;\n\n        base_lines_aggregated_values = this.aggregate_base_lines_tax_details(base_lines, subtotal_grouping_function);\n        values_per_grouping_key = this.aggregate_base_lines_aggregated_values(base_lines_aggregated_values);\n\n        for (const values of Object.values(values_per_grouping_key)) {\n            const subtotal_label = values.grouping_key || untaxed_amount_subtotal_label;\n            if (!(subtotal_label in subtotals)) {\n                subtotals[subtotal_label] = {\n                    tax_groups: [],\n                    tax_amount_currency: 0.0,\n                    tax_amount: 0.0,\n                    base_amount_currency: 0.0,\n                    base_amount: 0.0,\n                };\n            }\n            const subtotal = subtotals[subtotal_label];\n            for (const key of ['base_amount_currency', 'base_amount', 'tax_amount_currency', 'tax_amount']) {\n                subtotal[key] += values[key];\n            }\n        }\n\n        // Tax groups.\n        const tax_group_grouping_function = (base_line, tax_data) => tax_data.tax.tax_group_id;\n\n        base_lines_aggregated_values = this.aggregate_base_lines_tax_details(base_lines, tax_group_grouping_function);\n        values_per_grouping_key = this.aggregate_base_lines_aggregated_values(base_lines_aggregated_values);\n\n        const sorted_total_per_tax_group = Object.values(values_per_grouping_key)\n            .filter(values => values.grouping_key)\n            .sort((a, b) => (a.grouping_key.sequence - b.grouping_key.sequence) || (a.grouping_key.id - b.grouping_key.id));\n\n        const encountered_base_amounts = new Set();\n        const subtotals_order = {};\n\n        for (const [order, values] of sorted_total_per_tax_group.entries()) {\n            const tax_group = values.grouping_key;\n\n            // Get all involved taxes in the tax group.\n            const involved_tax_ids = new Set();\n            const involved_amount_types = new Set();\n            const involved_price_include = new Set();\n            values.base_line_x_taxes_data.forEach(([base_line, taxes_data]) => {\n                taxes_data.forEach(tax_data => {\n                    const tax = tax_data.tax;\n                    involved_tax_ids.add(tax.id);\n                    involved_amount_types.add(tax.amount_type);\n                    involved_price_include.add(tax.price_include);\n                });\n            });\n\n            // Compute the display base amounts.\n            let display_base_amount = values.base_amount;\n            let display_base_amount_currency = values.base_amount_currency;\n            if (involved_amount_types.size === 1 && involved_amount_types.has(\"fixed\")) {\n                display_base_amount = null;\n                display_base_amount_currency = null;\n            } else if (\n                involved_amount_types.size === 1\n                && involved_amount_types.has(\"division\")\n                && involved_price_include.size === 1\n                && involved_price_include.has(true)\n            ) {\n                values.base_line_x_taxes_data.forEach(([base_line, _taxes_data]) => {\n                    base_line.tax_details.taxes_data.forEach(tax_data => {\n                        if (tax_data.tax.amount_type === 'division') {\n                            display_base_amount_currency += tax_data.tax_amount_currency;\n                            display_base_amount += tax_data.tax_amount;\n                        }\n                    });\n                });\n            }\n\n            if (display_base_amount_currency !== null) {\n                encountered_base_amounts.add(parseFloat(display_base_amount_currency.toFixed(currency.decimal_places)));\n            }\n\n            // Order of the subtotals.\n            const preceding_subtotal = tax_group.preceding_subtotal || untaxed_amount_subtotal_label;\n            if (!(preceding_subtotal in subtotals_order)) {\n                subtotals_order[preceding_subtotal] = order;\n            }\n\n            subtotals[preceding_subtotal].tax_groups.push({\n                id: tax_group.id,\n                involved_tax_ids: Array.from(involved_tax_ids),\n                tax_amount_currency: values.tax_amount_currency,\n                tax_amount: values.tax_amount,\n                base_amount_currency: values.base_amount_currency,\n                base_amount: values.base_amount,\n                display_base_amount_currency,\n                display_base_amount,\n                group_name: tax_group.name,\n                group_label: tax_group.pos_receipt_label,\n            });\n        }\n\n        // Cash rounding\n        const cash_rounding_lines = base_lines.filter(base_line => base_line.special_type === 'cash_rounding');\n        if (cash_rounding_lines.length) {\n            tax_totals_summary.cash_rounding_base_amount_currency = 0.0;\n            tax_totals_summary.cash_rounding_base_amount = 0.0;\n            cash_rounding_lines.forEach(base_line => {\n                const tax_details = base_line.tax_details;\n                tax_totals_summary.cash_rounding_base_amount_currency += tax_details.total_excluded_currency;\n                tax_totals_summary.cash_rounding_base_amount += tax_details.total_excluded;\n            });\n        } else if (cash_rounding !== null) {\n            const strategy = cash_rounding.strategy;\n            const cash_rounding_pd = cash_rounding.rounding;\n            const total_amount_currency = tax_totals_summary.base_amount_currency + tax_totals_summary.tax_amount_currency;\n            const total_amount = tax_totals_summary.base_amount + tax_totals_summary.tax_amount;\n            const expected_total_amount_currency = roundPrecision(total_amount_currency, cash_rounding_pd);\n            const cash_rounding_base_amount_currency = expected_total_amount_currency - total_amount_currency;\n            if (!floatIsZero(cash_rounding_base_amount_currency, currency.decimal_places)) {\n                const rate = total_amount ? Math.abs(total_amount_currency / total_amount) : 0.0;\n                const cash_rounding_base_amount = rate ? roundPrecision(cash_rounding_base_amount_currency / rate, company_pd) : 0.0;\n                if (strategy === 'add_invoice_line') {\n                    tax_totals_summary.cash_rounding_base_amount_currency = cash_rounding_base_amount_currency;\n                    tax_totals_summary.cash_rounding_base_amount = cash_rounding_base_amount;\n                    tax_totals_summary.base_amount_currency += cash_rounding_base_amount_currency;\n                    tax_totals_summary.base_amount += cash_rounding_base_amount;\n                    subtotals[untaxed_amount_subtotal_label].base_amount_currency += cash_rounding_base_amount_currency;\n                    subtotals[untaxed_amount_subtotal_label].base_amount += cash_rounding_base_amount;\n                } else if (strategy === 'biggest_tax') {\n                    const [max_subtotal, max_tax_group] = Array.from(Object.values(subtotals))\n                        .flatMap(subtotal => subtotal.tax_groups.map(tax_group => [subtotal, tax_group]))\n                        .reduce((a, b) => (b[1].tax_amount_currency > a[1].tax_amount_currency ? b : a));\n\n                    max_tax_group.tax_amount_currency += cash_rounding_base_amount_currency;\n                    max_tax_group.tax_amount += cash_rounding_base_amount;\n                    max_subtotal.tax_amount_currency += cash_rounding_base_amount_currency;\n                    max_subtotal.tax_amount += cash_rounding_base_amount;\n                    tax_totals_summary.tax_amount_currency += cash_rounding_base_amount_currency;\n                    tax_totals_summary.tax_amount += cash_rounding_base_amount;\n                }\n            }\n        }\n\n        // Misc.\n        const ordered_subtotals = Array.from(Object.entries(subtotals))\n            .sort((a, b) => (subtotals_order[a[0]] || 0) - (subtotals_order[b[0]] || 0));\n        ordered_subtotals.forEach(([subtotal_label, subtotal]) => {\n            subtotal.name = subtotal_label;\n            tax_totals_summary.subtotals.push(subtotal);\n        });\n\n        tax_totals_summary.same_tax_base = encountered_base_amounts.size === 1;\n\n        // Total amount.\n        tax_totals_summary.total_amount_currency = tax_totals_summary.base_amount_currency + tax_totals_summary.tax_amount_currency;\n        tax_totals_summary.total_amount = tax_totals_summary.base_amount + tax_totals_summary.tax_amount;\n\n        return tax_totals_summary;\n    },\n\n    // -------------------------------------------------------------------------\n    // EDI HELPERS\n    // -------------------------------------------------------------------------\n\n    aggregate_base_line_tax_details(base_line, grouping_function) {\n        const values_per_grouping_key = {};\n        const tax_details = base_line.tax_details;\n        const taxes_data = tax_details.taxes_data;\n\n        for (const tax_data of taxes_data) {\n            let raw_grouping_key = grouping_function(base_line, tax_data);\n            let grouping_key = raw_grouping_key;\n\n            // Handle dictionary-like keys (converted to string in JS)\n            if (typeof grouping_key === 'object') {\n                grouping_key = JSON.stringify(raw_grouping_key);\n            }\n\n            // Base amount\n            if(!(grouping_key in values_per_grouping_key)){\n                values_per_grouping_key[grouping_key] = {\n                    base_amount_currency: tax_data.base_amount_currency,\n                    base_amount: tax_data.base_amount,\n                    raw_base_amount_currency: tax_data.raw_base_amount_currency,\n                    raw_base_amount: tax_data.raw_base_amount,\n                    tax_amount_currency: 0.0,\n                    tax_amount: 0.0,\n                    raw_tax_amount_currency: 0.0,\n                    raw_tax_amount: 0.0,\n                    taxes_data: [],\n                    grouping_key: raw_grouping_key\n                };\n            }\n            const values = values_per_grouping_key[grouping_key];\n            values.taxes_data.push(tax_data);\n\n            // Tax amount\n            values.tax_amount_currency += tax_data.tax_amount_currency;\n            values.tax_amount += tax_data.tax_amount;\n            values.raw_tax_amount_currency += tax_data.raw_tax_amount_currency;\n            values.raw_tax_amount += tax_data.raw_tax_amount;\n        }\n\n        if (!taxes_data.length) {\n            values_per_grouping_key[null] = {\n                base_amount_currency: tax_details.total_excluded_currency,\n                base_amount: tax_details.total_excluded,\n                raw_base_amount_currency: tax_details.raw_total_excluded_currency,\n                raw_base_amount: tax_details.raw_total_excluded,\n                tax_amount_currency: 0.0,\n                tax_amount: 0.0,\n                raw_tax_amount_currency: 0.0,\n                raw_tax_amount: 0.0,\n                taxes_data: [],\n                grouping_key: null\n            };\n        }\n\n        return values_per_grouping_key;\n    },\n\n    aggregate_base_lines_tax_details(base_lines, grouping_function) {\n        return base_lines.map(base_line => [base_line, this.aggregate_base_line_tax_details(base_line, grouping_function)]);\n    },\n\n    aggregate_base_lines_aggregated_values(base_lines_aggregated_values) {\n        const default_float_fields = new Set([\n            'base_amount_currency',\n            'base_amount',\n            'raw_base_amount_currency',\n            'raw_base_amount',\n            'tax_amount_currency',\n            'tax_amount',\n            'raw_tax_amount_currency',\n            'raw_tax_amount'\n        ]);\n        const values_per_grouping_key = {};\n        for (const [base_line, aggregated_values] of base_lines_aggregated_values) {\n            for (const [raw_grouping_key, values] of Object.entries(aggregated_values)) {\n                const grouping_key = values.grouping_key;\n\n                if(!(raw_grouping_key in values_per_grouping_key)){\n                    const initial_values = values_per_grouping_key[raw_grouping_key] = {\n                        base_line_x_taxes_data: [],\n                        grouping_key: grouping_key,\n                    };\n                    default_float_fields.forEach(field => {\n                        initial_values[field] = 0.0;\n                    });\n                }\n                const agg_values = values_per_grouping_key[raw_grouping_key];\n                default_float_fields.forEach(field => {\n                    agg_values[field] += values[field];\n                });\n                agg_values.base_line_x_taxes_data.push([base_line, values.taxes_data]);\n            }\n        }\n\n        return values_per_grouping_key;\n    },\n\n};\n", "import { useAutoresize } from \"@web/core/utils/autoresize\";\n\n/**\n * This overriden version of the resizeTextArea method is specificly done for the product_label_section_and_note widget\n * His necessity is found in the fact that the cell of said widget doesn't contain only the input or textarea to resize\n * but also another node containing the name of the product if said data is available. This means that the autoresize\n * method which sets the height of the parent cell should sometimes add an additional row to the parent cell so that\n * no text overflows\n *\n * @param {Ref} ref\n */\nexport function useProductAndLabelAutoresize(ref, options = {}) {\n    useAutoresize(ref, { onResize: productAndLabelResizeTextArea, ...options });\n}\n\nexport function productAndLabelResizeTextArea(textarea, options = {}) {\n    const style = window.getComputedStyle(textarea);\n    if (options.targetParentName) {\n        let target = textarea.parentElement;\n        let shouldContinue = true;\n        while (target && shouldContinue) {\n            const totalParentHeight = Array.from(target.children).reduce((total, child) => {\n                const childHeight = child.style.height || style.lineHeight;\n                return total + parseFloat(childHeight);\n            }, 0);\n            target.style.height = `${totalParentHeight}px`;\n            if (target.getAttribute(\"name\") === options.targetParentName) {\n                shouldContinue = false;\n            }\n            target = target.parentElement;\n        }\n    }\n}\n", "// Generated by CoffeeScript 1.7.1\n(function() {\n  var $, cardFromNumber, cardFromType, cards, defaultFormat, formatBackCardNumber, formatBackExpiry, formatCardNumber, formatExpiry, formatForwardExpiry, formatForwardSlashAndSpace, hasTextSelected, luhnCheck, reFormatCVC, reFormatCardNumber, reFormatExpiry, reFormatNumeric, replaceFullWidthChars, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric, safeVal, setCardType,\n    __slice = [].slice,\n    __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };\n\n  $ = window.jQuery || window.Zepto || window.$;\n\n  $.payment = {};\n\n  $.payment.fn = {};\n\n  $.fn.payment = function() {\n    var args, method;\n    method = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];\n    return $.payment.fn[method].apply(this, args);\n  };\n\n  defaultFormat = /(\\d{1,4})/g;\n\n  $.payment.cards = cards = [\n    {\n      type: 'maestro',\n      patterns: [5018, 502, 503, 506, 56, 58, 639, 6220, 67],\n      format: defaultFormat,\n      length: [12, 13, 14, 15, 16, 17, 18, 19],\n      cvcLength: [3],\n      luhn: true\n    }, {\n      type: 'forbrugsforeningen',\n      patterns: [600],\n      format: defaultFormat,\n      length: [16],\n      cvcLength: [3],\n      luhn: true\n    }, {\n      type: 'dankort',\n      patterns: [5019],\n      format: defaultFormat,\n      length: [16],\n      cvcLength: [3],\n      luhn: true\n    }, {\n      type: 'visa',\n      patterns: [4],\n      format: defaultFormat,\n      length: [13, 16],\n      cvcLength: [3],\n      luhn: true\n    }, {\n      type: 'mastercard',\n      patterns: [51, 52, 53, 54, 55, 22, 23, 24, 25, 26, 27],\n      format: defaultFormat,\n      length: [16],\n      cvcLength: [3],\n      luhn: true\n    }, {\n      type: 'amex',\n      patterns: [34, 37],\n      format: /(\\d{1,4})(\\d{1,6})?(\\d{1,5})?/,\n      length: [15],\n      cvcLength: [3, 4],\n      luhn: true\n    }, {\n      type: 'dinersclub',\n      patterns: [30, 36, 38, 39],\n      format: /(\\d{1,4})(\\d{1,6})?(\\d{1,4})?/,\n      length: [14],\n      cvcLength: [3],\n      luhn: true\n    }, {\n      type: 'discover',\n      patterns: [60, 64, 65, 622],\n      format: defaultFormat,\n      length: [16],\n      cvcLength: [3],\n      luhn: true\n    }, {\n      type: 'unionpay',\n      patterns: [62, 88],\n      format: defaultFormat,\n      length: [16, 17, 18, 19],\n      cvcLength: [3],\n      luhn: false\n    }, {\n      type: 'jcb',\n      patterns: [35],\n      format: defaultFormat,\n      length: [16],\n      cvcLength: [3],\n      luhn: true\n    }\n  ];\n\n  cardFromNumber = function(num) {\n    var card, p, pattern, _i, _j, _len, _len1, _ref;\n    num = (num + '').replace(/\\D/g, '');\n    for (_i = 0, _len = cards.length; _i < _len; _i++) {\n      card = cards[_i];\n      _ref = card.patterns;\n      for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) {\n        pattern = _ref[_j];\n        p = pattern + '';\n        if (num.substr(0, p.length) === p) {\n          return card;\n        }\n      }\n    }\n  };\n\n  cardFromType = function(type) {\n    var card, _i, _len;\n    for (_i = 0, _len = cards.length; _i < _len; _i++) {\n      card = cards[_i];\n      if (card.type === type) {\n        return card;\n      }\n    }\n  };\n\n  luhnCheck = function(num) {\n    var digit, digits, odd, sum, _i, _len;\n    odd = true;\n    sum = 0;\n    digits = (num + '').split('').reverse();\n    for (_i = 0, _len = digits.length; _i < _len; _i++) {\n      digit = digits[_i];\n      digit = parseInt(digit, 10);\n      if ((odd = !odd)) {\n        digit *= 2;\n      }\n      if (digit > 9) {\n        digit -= 9;\n      }\n      sum += digit;\n    }\n    return sum % 10 === 0;\n  };\n\n  hasTextSelected = function($target) {\n    var _ref;\n    if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== $target.prop('selectionEnd')) {\n      return true;\n    }\n    if ((typeof document !== \"undefined\" && document !== null ? (_ref = document.selection) != null ? _ref.createRange : void 0 : void 0) != null) {\n      if (document.selection.createRange().text) {\n        return true;\n      }\n    }\n    return false;\n  };\n\n  safeVal = function(value, $target) {\n    var currPair, cursor, digit, error, last, prevPair;\n    try {\n      cursor = $target.prop('selectionStart');\n    } catch (_error) {\n      error = _error;\n      cursor = null;\n    }\n    last = $target.val();\n    $target.val(value);\n    if (cursor !== null && $target.is(\":focus\")) {\n      if (cursor === last.length) {\n        cursor = value.length;\n      }\n      if (last !== value) {\n        prevPair = last.slice(cursor - 1, +cursor + 1 || 9e9);\n        currPair = value.slice(cursor - 1, +cursor + 1 || 9e9);\n        digit = value[cursor];\n        if (/\\d/.test(digit) && prevPair === (\"\" + digit + \" \") && currPair === (\" \" + digit)) {\n          cursor = cursor + 1;\n        }\n      }\n      $target.prop('selectionStart', cursor);\n      return $target.prop('selectionEnd', cursor);\n    }\n  };\n\n  replaceFullWidthChars = function(str) {\n    var chars, chr, fullWidth, halfWidth, idx, value, _i, _len;\n    if (str == null) {\n      str = '';\n    }\n    fullWidth = '\\uff10\\uff11\\uff12\\uff13\\uff14\\uff15\\uff16\\uff17\\uff18\\uff19';\n    halfWidth = '0123456789';\n    value = '';\n    chars = str.split('');\n    for (_i = 0, _len = chars.length; _i < _len; _i++) {\n      chr = chars[_i];\n      idx = fullWidth.indexOf(chr);\n      if (idx > -1) {\n        chr = halfWidth[idx];\n      }\n      value += chr;\n    }\n    return value;\n  };\n\n  reFormatNumeric = function(e) {\n    var $target;\n    $target = $(e.currentTarget);\n    return setTimeout(function() {\n      var value;\n      value = $target.val();\n      value = replaceFullWidthChars(value);\n      value = value.replace(/\\D/g, '');\n      return safeVal(value, $target);\n    });\n  };\n\n  reFormatCardNumber = function(e) {\n    var $target;\n    $target = $(e.currentTarget);\n    return setTimeout(function() {\n      var value;\n      value = $target.val();\n      value = replaceFullWidthChars(value);\n      value = $.payment.formatCardNumber(value);\n      return safeVal(value, $target);\n    });\n  };\n\n  formatCardNumber = function(e) {\n    var $target, card, digit, length, re, upperLength, value;\n    digit = String.fromCharCode(e.which);\n    if (!/^\\d+$/.test(digit)) {\n      return;\n    }\n    $target = $(e.currentTarget);\n    value = $target.val();\n    card = cardFromNumber(value + digit);\n    length = (value.replace(/\\D/g, '') + digit).length;\n    upperLength = 16;\n    if (card) {\n      upperLength = card.length[card.length.length - 1];\n    }\n    if (length >= upperLength) {\n      return;\n    }\n    if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {\n      return;\n    }\n    if (card && card.type === 'amex') {\n      re = /^(\\d{4}|\\d{4}\\s\\d{6})$/;\n    } else {\n      re = /(?:^|\\s)(\\d{4})$/;\n    }\n    if (re.test(value)) {\n      e.preventDefault();\n      return setTimeout(function() {\n        return $target.val(value + ' ' + digit);\n      });\n    } else if (re.test(value + digit)) {\n      e.preventDefault();\n      return setTimeout(function() {\n        return $target.val(value + digit + ' ');\n      });\n    }\n  };\n\n  formatBackCardNumber = function(e) {\n    var $target, value;\n    $target = $(e.currentTarget);\n    value = $target.val();\n    if (e.which !== 8) {\n      return;\n    }\n    if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {\n      return;\n    }\n    if (/\\d\\s$/.test(value)) {\n      e.preventDefault();\n      return setTimeout(function() {\n        return $target.val(value.replace(/\\d\\s$/, ''));\n      });\n    } else if (/\\s\\d?$/.test(value)) {\n      e.preventDefault();\n      return setTimeout(function() {\n        return $target.val(value.replace(/\\d$/, ''));\n      });\n    }\n  };\n\n  reFormatExpiry = function(e) {\n    var $target;\n    $target = $(e.currentTarget);\n    return setTimeout(function() {\n      var value;\n      value = $target.val();\n      value = replaceFullWidthChars(value);\n      value = $.payment.formatExpiry(value);\n      return safeVal(value, $target);\n    });\n  };\n\n  formatExpiry = function(e) {\n    var $target, digit, val;\n    digit = String.fromCharCode(e.which);\n    if (!/^\\d+$/.test(digit)) {\n      return;\n    }\n    $target = $(e.currentTarget);\n    val = $target.val() + digit;\n    if (/^\\d$/.test(val) && (val !== '0' && val !== '1')) {\n      e.preventDefault();\n      return setTimeout(function() {\n        return $target.val(\"0\" + val + \" / \");\n      });\n    } else if (/^\\d\\d$/.test(val)) {\n      e.preventDefault();\n      return setTimeout(function() {\n        var m1, m2;\n        m1 = parseInt(val[0], 10);\n        m2 = parseInt(val[1], 10);\n        if (m2 > 2 && m1 !== 0) {\n          return $target.val(\"0\" + m1 + \" / \" + m2);\n        } else {\n          return $target.val(\"\" + val + \" / \");\n        }\n      });\n    }\n  };\n\n  formatForwardExpiry = function(e) {\n    var $target, digit, val;\n    digit = String.fromCharCode(e.which);\n    if (!/^\\d+$/.test(digit)) {\n      return;\n    }\n    $target = $(e.currentTarget);\n    val = $target.val();\n    if (/^\\d\\d$/.test(val)) {\n      return $target.val(\"\" + val + \" / \");\n    }\n  };\n\n  formatForwardSlashAndSpace = function(e) {\n    var $target, val, which;\n    which = String.fromCharCode(e.which);\n    if (!(which === '/' || which === ' ')) {\n      return;\n    }\n    $target = $(e.currentTarget);\n    val = $target.val();\n    if (/^\\d$/.test(val) && val !== '0') {\n      return $target.val(\"0\" + val + \" / \");\n    }\n  };\n\n  formatBackExpiry = function(e) {\n    var $target, value;\n    $target = $(e.currentTarget);\n    value = $target.val();\n    if (e.which !== 8) {\n      return;\n    }\n    if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {\n      return;\n    }\n    if (/\\d\\s\\/\\s$/.test(value)) {\n      e.preventDefault();\n      return setTimeout(function() {\n        return $target.val(value.replace(/\\d\\s\\/\\s$/, ''));\n      });\n    }\n  };\n\n  reFormatCVC = function(e) {\n    var $target;\n    $target = $(e.currentTarget);\n    return setTimeout(function() {\n      var value;\n      value = $target.val();\n      value = replaceFullWidthChars(value);\n      value = value.replace(/\\D/g, '').slice(0, 4);\n      return safeVal(value, $target);\n    });\n  };\n\n  restrictNumeric = function(e) {\n    var input;\n    if (e.metaKey || e.ctrlKey) {\n      return true;\n    }\n    if (e.which === 32) {\n      return false;\n    }\n    if (e.which === 0) {\n      return true;\n    }\n    if (e.which < 33) {\n      return true;\n    }\n    input = String.fromCharCode(e.which);\n    return !!/[\\d\\s]/.test(input);\n  };\n\n  restrictCardNumber = function(e) {\n    var $target, card, digit, value;\n    $target = $(e.currentTarget);\n    digit = String.fromCharCode(e.which);\n    if (!/^\\d+$/.test(digit)) {\n      return;\n    }\n    if (hasTextSelected($target)) {\n      return;\n    }\n    value = ($target.val() + digit).replace(/\\D/g, '');\n    card = cardFromNumber(value);\n    if (card) {\n      return value.length <= card.length[card.length.length - 1];\n    } else {\n      return value.length <= 16;\n    }\n  };\n\n  restrictExpiry = function(e) {\n    var $target, digit, value;\n    $target = $(e.currentTarget);\n    digit = String.fromCharCode(e.which);\n    if (!/^\\d+$/.test(digit)) {\n      return;\n    }\n    if (hasTextSelected($target)) {\n      return;\n    }\n    value = $target.val() + digit;\n    value = value.replace(/\\D/g, '');\n    if (value.length > 6) {\n      return false;\n    }\n  };\n\n  restrictCVC = function(e) {\n    var $target, digit, val;\n    $target = $(e.currentTarget);\n    digit = String.fromCharCode(e.which);\n    if (!/^\\d+$/.test(digit)) {\n      return;\n    }\n    if (hasTextSelected($target)) {\n      return;\n    }\n    val = $target.val() + digit;\n    return val.length <= 4;\n  };\n\n  setCardType = function(e) {\n    var $target, allTypes, card, cardType, val;\n    $target = $(e.currentTarget);\n    val = $target.val();\n    cardType = $.payment.cardType(val) || 'unknown';\n    if (!$target.hasClass(cardType)) {\n      allTypes = (function() {\n        var _i, _len, _results;\n        _results = [];\n        for (_i = 0, _len = cards.length; _i < _len; _i++) {\n          card = cards[_i];\n          _results.push(card.type);\n        }\n        return _results;\n      })();\n      $target.removeClass('unknown');\n      $target.removeClass(allTypes.join(' '));\n      $target.addClass(cardType);\n      $target.toggleClass('identified', cardType !== 'unknown');\n      return $target.trigger('payment.cardType', cardType);\n    }\n  };\n\n  $.payment.fn.formatCardCVC = function() {\n    this.on('keypress', restrictNumeric);\n    this.on('keypress', restrictCVC);\n    this.on('paste', reFormatCVC);\n    this.on('change', reFormatCVC);\n    this.on('input', reFormatCVC);\n    return this;\n  };\n\n  $.payment.fn.formatCardExpiry = function() {\n    this.on('keypress', restrictNumeric);\n    this.on('keypress', restrictExpiry);\n    this.on('keypress', formatExpiry);\n    this.on('keypress', formatForwardSlashAndSpace);\n    this.on('keypress', formatForwardExpiry);\n    this.on('keydown', formatBackExpiry);\n    this.on('change', reFormatExpiry);\n    this.on('input', reFormatExpiry);\n    return this;\n  };\n\n  $.payment.fn.formatCardNumber = function() {\n    this.on('keypress', restrictNumeric);\n    this.on('keypress', restrictCardNumber);\n    this.on('keypress', formatCardNumber);\n    this.on('keydown', formatBackCardNumber);\n    this.on('keyup', setCardType);\n    this.on('paste', reFormatCardNumber);\n    this.on('change', reFormatCardNumber);\n    this.on('input', reFormatCardNumber);\n    this.on('input', setCardType);\n    return this;\n  };\n\n  $.payment.fn.restrictNumeric = function() {\n    this.on('keypress', restrictNumeric);\n    this.on('paste', reFormatNumeric);\n    this.on('change', reFormatNumeric);\n    this.on('input', reFormatNumeric);\n    return this;\n  };\n\n  $.payment.fn.cardExpiryVal = function() {\n    return $.payment.cardExpiryVal($(this).val());\n  };\n\n  $.payment.cardExpiryVal = function(value) {\n    var month, prefix, year, _ref;\n    _ref = value.split(/[\\s\\/]+/, 2), month = _ref[0], year = _ref[1];\n    if ((year != null ? year.length : void 0) === 2 && /^\\d+$/.test(year)) {\n      prefix = (new Date).getFullYear();\n      prefix = prefix.toString().slice(0, 2);\n      year = prefix + year;\n    }\n    month = parseInt(month, 10);\n    year = parseInt(year, 10);\n    return {\n      month: month,\n      year: year\n    };\n  };\n\n  $.payment.validateCardNumber = function(num) {\n    var card, _ref;\n    num = (num + '').replace(/\\s+|-/g, '');\n    if (!/^\\d+$/.test(num)) {\n      return false;\n    }\n    card = cardFromNumber(num);\n    if (!card) {\n      return false;\n    }\n    return (_ref = num.length, __indexOf.call(card.length, _ref) >= 0) && (card.luhn === false || luhnCheck(num));\n  };\n\n  $.payment.validateCardExpiry = function(month, year) {\n    var currentTime, expiry, _ref;\n    if (typeof month === 'object' && 'month' in month) {\n      _ref = month, month = _ref.month, year = _ref.year;\n    }\n    if (!(month && year)) {\n      return false;\n    }\n    month = $.trim(month);\n    year = $.trim(year);\n    if (!/^\\d+$/.test(month)) {\n      return false;\n    }\n    if (!/^\\d+$/.test(year)) {\n      return false;\n    }\n    if (!((1 <= month && month <= 12))) {\n      return false;\n    }\n    if (year.length === 2) {\n      if (year < 70) {\n        year = \"20\" + year;\n      } else {\n        year = \"19\" + year;\n      }\n    }\n    if (year.length !== 4) {\n      return false;\n    }\n    expiry = new Date(year, month);\n    currentTime = new Date;\n    expiry.setMonth(expiry.getMonth() - 1);\n    expiry.setMonth(expiry.getMonth() + 1, 1);\n    return expiry > currentTime;\n  };\n\n  $.payment.validateCardCVC = function(cvc, type) {\n    var card, _ref;\n    cvc = $.trim(cvc);\n    if (!/^\\d+$/.test(cvc)) {\n      return false;\n    }\n    card = cardFromType(type);\n    if (card != null) {\n      return _ref = cvc.length, __indexOf.call(card.cvcLength, _ref) >= 0;\n    } else {\n      return cvc.length >= 3 && cvc.length <= 4;\n    }\n  };\n\n  $.payment.cardType = function(num) {\n    var _ref;\n    if (!num) {\n      return null;\n    }\n    return ((_ref = cardFromNumber(num)) != null ? _ref.type : void 0) || null;\n  };\n\n  $.payment.formatCardNumber = function(num) {\n    var card, groups, upperLength, _ref;\n    num = num.replace(/\\D/g, '');\n    card = cardFromNumber(num);\n    if (!card) {\n      return num;\n    }\n    upperLength = card.length[card.length.length - 1];\n    num = num.slice(0, upperLength);\n    if (card.format.global) {\n      return (_ref = num.match(card.format)) != null ? _ref.join(' ') : void 0;\n    } else {\n      groups = card.format.exec(num);\n      if (groups == null) {\n        return;\n      }\n      groups.shift();\n      groups = $.grep(groups, function(n) {\n        return n;\n      });\n      return groups.join(' ');\n    }\n  };\n\n  $.payment.formatExpiry = function(expiry) {\n    var mon, parts, sep, year;\n    parts = expiry.match(/^\\D*(\\d{1,2})(\\D+)?(\\d{1,4})?/);\n    if (!parts) {\n      return '';\n    }\n    mon = parts[1] || '';\n    sep = parts[2] || '';\n    year = parts[3] || '';\n    if (year.length > 0) {\n      sep = ' / ';\n    } else if (sep === ' /') {\n      mon = mon.substring(0, 1);\n      sep = '';\n    } else if (mon.length === 2 || sep.length > 0) {\n      sep = ' / ';\n    } else if (mon.length === 1 && (mon !== '0' && mon !== '1')) {\n      mon = \"0\" + mon;\n      sep = ' / ';\n    }\n    return mon + sep + year;\n  };\n\n}).call(this);\n", "/** @odoo-module */\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { Component } from '@odoo/owl';\n\npublicWidget.registry.PaymentExpressCheckoutForm = publicWidget.Widget.extend({\n    selector: 'form[name=\"o_payment_express_checkout_form\"]',\n\n    /**\n     * @override\n     */\n    start: async function () {\n        await this._super(...arguments);\n        this.paymentContext = {};\n        Object.assign(this.paymentContext, this.el.dataset);\n        this.paymentContext.shippingInfoRequired = !!this.paymentContext['shippingInfoRequired'];\n        const expressCheckoutForms = this._getExpressCheckoutForms();\n        for (const expressCheckoutForm of expressCheckoutForms) {\n            await this._prepareExpressCheckoutForm(expressCheckoutForm.dataset);\n        }\n        // Monitor updates of the amount on eCommerce's cart pages.\n        Component.env.bus.addEventListener('cart_amount_changed', (ev) => this._updateAmount(...ev.detail));\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Return all express checkout forms found on the page.\n     *\n     * @private\n     * @return {NodeList} - All express checkout forms found on the page.\n     */\n    _getExpressCheckoutForms() {\n        return document.querySelectorAll(\n            'form[name=\"o_payment_express_checkout_form\"] div[name=\"o_express_checkout_container\"]'\n        );\n    },\n\n    /**\n     * Prepare the provider-specific express checkout form based on the provided data.\n     *\n     * For a provider to manage an express checkout form, it must override this method.\n     *\n     * @private\n     * @param {Object} providerData - The provider-specific data.\n     * @return {void}\n     */\n    async _prepareExpressCheckoutForm(providerData) {},\n\n    /**\n     * Prepare the params for the RPC to the transaction route.\n     *\n     * @private\n     * @param {number} providerId - The id of the provider handling the transaction.\n     * @returns {object} - The transaction route params.\n     */\n    _prepareTransactionRouteParams(providerId) {\n        return {\n            'provider_id': parseInt(providerId),\n            'payment_method_id': parseInt(this.paymentContext['paymentMethodUnknownId']),\n            'token_id': null,\n            'flow': 'direct',\n            'tokenization_requested': false,\n            'landing_route': this.paymentContext['landingRoute'],\n            'access_token': this.paymentContext['accessToken'],\n            'csrf_token': odoo.csrf_token,\n        };\n    },\n\n    /**\n     * Update the amount of the express checkout form.\n     *\n     * For a provider to manage an express form, it must override this method.\n     *\n     * @private\n     * @param {number} newAmount - The new amount.\n     * @param {number} newMinorAmount - The new minor amount.\n     * @return {void}\n     */\n    _updateAmount(newAmount, newMinorAmount) {\n        this.paymentContext.amount = parseFloat(newAmount);\n        this.paymentContext.minorAmount = parseInt(newMinorAmount);\n        this._getExpressCheckoutForms().forEach(form => {\n            if (newAmount == 0) {\n                form.classList.add('d-none')}\n            else {\n                form.classList.remove('d-none')\n            }\n        })\n    },\n\n});\n\nexport const paymentExpressCheckoutForm = publicWidget.registry.PaymentExpressCheckoutForm;\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { Component } from \"@odoo/owl\";\n\npublicWidget.registry.PaymentButton = publicWidget.Widget.extend({\n    selector: 'button[name=\"o_payment_submit_button\"]',\n\n    async start() {\n        await this._super(...arguments);\n        this.paymentButton = this.el;\n        this.iconClass = this.paymentButton.dataset.iconClass;\n        this._enable();\n        Component.env.bus.addEventListener('enablePaymentButton', this._enable.bind(this));\n        Component.env.bus.addEventListener('disablePaymentButton',this._disable.bind(this));\n        Component.env.bus.addEventListener('hidePaymentButton', this._hide.bind(this));\n        Component.env.bus.addEventListener('showPaymentButton', this._show.bind(this));\n    },\n\n    /**\n     * Check if the payment button can be enabled and do it if so.\n     *\n     * @private\n     * @return {void}\n     */\n    _enable() {\n        if (this._canSubmit()) {\n            this.paymentButton.disabled = false;\n        }\n    },\n\n    /**\n     * Check whether the payment form can be submitted, i.e. whether exactly one payment option is\n     * selected.\n     *\n     * For a module to add a condition on the submission of the form, it must override this method\n     * and return whether both this method's condition and the override method's condition are met.\n     *\n     * @private\n     * @return {boolean} Whether the form can be submitted.\n     */\n    _canSubmit() {\n        const paymentForm = document.querySelector('#o_payment_form');\n        if (!paymentForm) {  // Payment form is not present.\n            return true; // Ignore the check.\n        }\n        return document.querySelectorAll('input[name=\"o_payment_radio\"]:checked').length === 1;\n    },\n\n    /**\n     * Disable the payment button.\n     *\n     * @private\n     * @return {void}\n     */\n    _disable() {\n        this.paymentButton.disabled = true;\n    },\n\n    /**\n     * Hide the payment button.\n     *\n     * @private\n     * @return {void}\n     */\n    _hide() {\n        this.paymentButton.classList.add('d-none');\n    },\n\n    /**\n     * Show the payment button.\n     *\n     * @private\n     * @return {void}\n     */\n    _show() {\n        this.paymentButton.classList.remove('d-none');\n    },\n\n});\nexport default publicWidget.registry.PaymentButton;\n", "/** @odoo-module **/\n\nimport { Component } from '@odoo/owl';\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { browser } from '@web/core/browser/browser';\nimport { ConfirmationDialog } from '@web/core/confirmation_dialog/confirmation_dialog';\nimport { _t } from '@web/core/l10n/translation';\nimport { renderToMarkup } from '@web/core/utils/render';\nimport { rpc, RPCError } from '@web/core/network/rpc';\n\npublicWidget.registry.PaymentForm = publicWidget.Widget.extend({\n    selector: '#o_payment_form',\n    events: Object.assign({}, publicWidget.Widget.prototype.events, {\n        'click [name=\"o_payment_radio\"]': '_selectPaymentOption',\n        'click [name=\"o_payment_delete_token\"]': '_fetchTokenData',\n        'click [name=\"o_payment_expand_button\"]': '_hideExpandButton',\n        'click [name=\"o_payment_submit_button\"]': '_submitForm',\n    }),\n\n    // #=== WIDGET LIFECYCLE ===#\n\n    /**\n     * @override\n     */\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n    },\n\n    /**\n     * @override\n     */\n    async start() {\n        // Synchronously initialize paymentContext before any await.\n        this.paymentContext = {};\n        Object.assign(this.paymentContext, this.el.dataset);\n\n        await this._super(...arguments);\n\n        // Expand the payment form of the selected payment option if there is only one.\n        const checkedRadio = document.querySelector('input[name=\"o_payment_radio\"]:checked');\n        if (checkedRadio) {\n            await this._expandInlineForm(checkedRadio);\n            this._enableButton(false);\n        } else {\n            this._setPaymentFlow(); // Initialize the payment flow to let providers overwrite it.\n        }\n\n        this.$('[data-bs-toggle=\"tooltip\"]').tooltip();\n    },\n\n    // #=== EVENT HANDLERS ===#\n\n    /**\n     * Open the inline form of the selected payment option, if any.\n     *\n     * @private\n     * @param {Event} ev\n     * @return {void}\n     */\n    async _selectPaymentOption(ev) {\n        // Show the inputs in case they have been hidden.\n        this._showInputs();\n\n        // Disable the submit button while preparing the inline form.\n        this._disableButton();\n\n        // Unfold and prepare the inline form of the selected payment option.\n        const checkedRadio = ev.target;\n        await this._expandInlineForm(checkedRadio);\n\n        // Re-enable the submit button after the inline form has been prepared.\n        this._enableButton(false);\n    },\n\n    /**\n     * Fetch data relative to the documents linked to the token and delegate them to the token\n     * deletion confirmation dialog.\n     *\n     * @private\n     * @param {Event} ev\n     * @return {void}\n     */\n    _fetchTokenData(ev) {\n        ev.preventDefault();\n\n        const linkedRadio = document.getElementById(ev.currentTarget.dataset['linkedRadio']);\n        const tokenId = this._getPaymentOptionId(linkedRadio);\n        this.orm.call(\n            'payment.token',\n            'get_linked_records_info',\n            [tokenId],\n        ).then(linkedRecordsInfo => {\n            this._challengeTokenDeletion(tokenId, linkedRecordsInfo);\n        }).catch(error => {\n            if (error instanceof RPCError) {\n                this._displayErrorDialog(\n                    _t(\"Cannot delete payment method\"), error.data.message\n                );\n            } else {\n                return Promise.reject(error);\n            }\n        });\n    },\n\n    /**\n     * Hide the button to expand the payment methods section once it has been clicked.\n     *\n     * @private\n     * @param {Event} ev\n     * @return {void}\n     */\n    _hideExpandButton(ev) {\n        ev.target.classList.add('d-none');\n    },\n\n    /**\n     * Update the payment context with the selected payment option and initiate its payment flow.\n     *\n     * @private\n     * @param {Event} ev\n     * @return {void}\n     */\n    async _submitForm(ev) {\n        ev.stopPropagation();\n        ev.preventDefault();\n\n        const checkedRadio = this.el.querySelector('input[name=\"o_payment_radio\"]:checked');\n\n        // Block the entire UI to prevent fiddling with other widgets.\n        this._disableButton(true);\n\n        // Initiate the payment flow of the selected payment option.\n        const flow = this.paymentContext.flow = this._getPaymentFlow(checkedRadio);\n        const paymentOptionId = this.paymentContext.paymentOptionId = this._getPaymentOptionId(\n            checkedRadio\n        );\n        if (flow === 'token' && this.paymentContext['assignTokenRoute']) { // Assign token flow.\n            await this._assignToken(paymentOptionId);\n        } else { // Both tokens and payment methods must process a payment operation.\n            const providerCode = this.paymentContext.providerCode = this._getProviderCode(\n                checkedRadio\n            );\n            const pmCode = this.paymentContext.paymentMethodCode = this._getPaymentMethodCode(\n                checkedRadio\n            );\n            this.paymentContext.providerId = this._getProviderId(checkedRadio);\n            if (this._getPaymentOptionType(checkedRadio) === 'token') {\n                this.paymentContext.tokenId = paymentOptionId;\n            } else { // 'payment_method'\n                this.paymentContext.paymentMethodId = paymentOptionId;\n            }\n            const inlineForm = this._getInlineForm(checkedRadio);\n            this.paymentContext.tokenizationRequested = inlineForm?.querySelector(\n                '[name=\"o_payment_tokenize_checkbox\"]'\n            )?.checked ?? this.paymentContext['mode'] === 'validation';\n            await this._initiatePaymentFlow(providerCode, paymentOptionId, pmCode, flow);\n        }\n    },\n\n    // #=== DOM MANIPULATION ===#\n\n    /**\n     * Check if the submit button can be enabled and do it if so.\n     *\n     * @private\n     * @param {boolean} unblockUI - Whether the UI should also be unblocked.\n     * @return {void}\n     */\n    _enableButton(unblockUI = true) {\n        Component.env.bus.trigger('enablePaymentButton');\n        if (unblockUI) {\n            this.call('ui', 'unblock');\n        }\n    },\n\n    /**\n     * Disable the submit button.\n     *\n     * @private\n     * @param {boolean} blockUI - Whether the UI should also be blocked.\n     * @return {void}\n     */\n    _disableButton(blockUI = false) {\n        Component.env.bus.trigger('disablePaymentButton');\n        if (blockUI) {\n            this.call('ui', 'block');\n        }\n    },\n\n    /**\n     * Show the tokenization checkbox, its label, and the submit button.\n     *\n     * @private\n     * @return {void}\n     */\n    _showInputs() {\n        // Show the tokenization checkbox and its label.\n        const tokenizeContainer = this.el.querySelector('[name=\"o_payment_tokenize_container\"]');\n        tokenizeContainer?.classList.remove('d-none');\n\n        // Show the submit button.\n        Component.env.bus.trigger('showPaymentButton');\n    },\n\n    /**\n     * Hide the tokenization checkbox, its label, and the submit button.\n     *\n     * The inputs should typically be hidden when the customer has to perform additional actions in\n     * the inline form. All inputs are automatically shown again when the customer selects another\n     * payment option.\n     *\n     * @private\n     * @return {void}\n     */\n    _hideInputs() {\n        // Hide the tokenization checkbox and its label.\n        const tokenizeContainer = this.el.querySelector('[name=\"o_payment_tokenize_container\"]');\n        tokenizeContainer?.classList.add('d-none');\n\n        // Hide the submit button.\n        Component.env.bus.trigger('hidePaymentButton');\n    },\n\n    /**\n     * Open the inline form of the selected payment option and collapse the others.\n     *\n     * @private\n     * @param {HTMLInputElement} radio - The radio button linked to the payment option.\n     * @return {void}\n     */\n    async _expandInlineForm(radio) {\n        this._collapseInlineForms(); // Collapse previously opened inline forms.\n        this._setPaymentFlow(); // Reset the payment flow to let providers overwrite it.\n\n        // Prepare the inline form of the selected payment option.\n        const providerId = this._getProviderId(radio);\n        const providerCode = this._getProviderCode(radio);\n        const paymentOptionId = this._getPaymentOptionId(radio);\n        const paymentMethodCode = this._getPaymentMethodCode(radio);\n        const flow = this._getPaymentFlow(radio);\n        await this._prepareInlineForm(\n            providerId, providerCode, paymentOptionId, paymentMethodCode, flow\n        );\n\n        // Display the prepared inline form if it is not empty.\n        const inlineForm = this._getInlineForm(radio);\n        if (inlineForm && inlineForm.children.length > 0) {\n            inlineForm.classList.remove('d-none');\n        }\n    },\n\n    /**\n     * Prepare the provider-specific inline form of the selected payment option.\n     *\n     * For a provider to manage an inline form, it must override this method and render the content\n     * of the form.\n     *\n     * @private\n     * @param {number} providerId - The id of the selected payment option's provider.\n     * @param {string} providerCode - The code of the selected payment option's provider.\n     * @param {number} paymentOptionId - The id of the selected payment option.\n     * @param {string} paymentMethodCode - The code of the selected payment method, if any.\n     * @param {string} flow - The online payment flow of the selected payment option.\n     * @return {void}\n     */\n    async _prepareInlineForm(providerId, providerCode, paymentOptionId, paymentMethodCode, flow) {},\n\n    /**\n     * Collapse all inline forms of the current widget.\n     *\n     * @private\n     * @return {void}\n     */\n    _collapseInlineForms() {\n        this.el.querySelectorAll('[name=\"o_payment_inline_form\"]').forEach(inlineForm => {\n            inlineForm.classList.add('d-none');\n        });\n    },\n\n    /**\n     * Display an error dialog.\n     *\n     * @private\n     * @param {string} title - The title of the dialog.\n     * @param {string} errorMessage - The error message.\n     * @return {void}\n     */\n    _displayErrorDialog(title, errorMessage = '') {\n        this.call('dialog', 'add', ConfirmationDialog, { title: title, body: errorMessage || \"\" });\n    },\n\n    /**\n     * Display the token deletion confirmation dialog.\n     *\n     * @private\n     * @param {number} tokenId - The id of the token whose deletion was requested.\n     * @param {object} linkedRecordsInfo - The data relative to the documents linked to the token.\n     * @return {void}\n     */\n    _challengeTokenDeletion(tokenId, linkedRecordsInfo) {\n        const body = renderToMarkup('payment.deleteTokenDialog', { linkedRecordsInfo });\n        this.call('dialog', 'add', ConfirmationDialog, {\n            title: _t(\"Warning!\"),\n            body,\n            confirmLabel: _t(\"Confirm Deletion\"),\n            confirm: () => this._archiveToken(tokenId),\n            cancel: () => {},\n        });\n    },\n\n    // #=== PAYMENT FLOW ===#\n\n    /**\n     * Set the payment flow for the selected payment option.\n     *\n     * For a provider to manage direct payments, it must call this method and set the payment flow\n     * when its payment option is selected.\n     *\n     * @private\n     * @param {string} flow - The flow for the selected payment option. Either 'redirect', 'direct',\n     *                        or 'token'\n     * @return {void}\n     */\n    _setPaymentFlow(flow = 'redirect') {\n        if (['redirect', 'direct', 'token'].includes(flow)) {\n            this.paymentContext.flow = flow;\n        } else {\n            console.warn(`The value ${flow} is not a supported flow. Falling back to redirect.`);\n            this.paymentContext.flow = 'redirect';\n        }\n    },\n\n    /**\n     * Assign the selected token to a document through the `assignTokenRoute`.\n     *\n     * @private\n     * @param {number} tokenId - The id of the token to assign.\n     * @return {void}\n     */\n    async _assignToken(tokenId) {\n        rpc(this.paymentContext['assignTokenRoute'], {\n            'token_id': tokenId,\n            'access_token': this.paymentContext['accessToken'],\n        }).then(() => {\n            window.location = this.paymentContext['landingRoute'];\n        }).catch(error => {\n            if (error instanceof RPCError) {\n                this._displayErrorDialog(_t(\"Cannot save payment method\"), error.data.message);\n                this._enableButton(); // The button has been disabled before initiating the flow.\n            } else {\n                return Promise.reject(error);\n            }\n        });\n    },\n\n    /**\n     * Make an RPC to initiate the payment flow by creating a new transaction.\n     *\n     * For a provider to do pre-processing work (e.g., perform checks on the form inputs), or to\n     * process the payment flow in its own terms (e.g., re-schedule the RPC to the transaction\n     * route), it must override this method.\n     *\n     * To alter the flow-specific processing, it is advised to override `_processRedirectFlow`,\n     * `_processDirectFlow`, or `_processTokenFlow` instead.\n     *\n     * @private\n     * @param {string} providerCode - The code of the selected payment option's provider.\n     * @param {number} paymentOptionId - The id of the selected payment option.\n     * @param {string} paymentMethodCode - The code of the selected payment method, if any.\n     * @param {string} flow - The payment flow of the selected payment option.\n     * @return {void}\n     */\n    async _initiatePaymentFlow(providerCode, paymentOptionId, paymentMethodCode, flow) {\n        // Create a transaction and retrieve its processing values.\n        await rpc(\n            this.paymentContext['transactionRoute'],\n            this._prepareTransactionRouteParams(),\n        ).then(processingValues => {\n            if (flow === 'redirect') {\n                this._processRedirectFlow(\n                    providerCode, paymentOptionId, paymentMethodCode, processingValues\n                );\n            } else if (flow === 'direct') {\n                this._processDirectFlow(\n                    providerCode, paymentOptionId, paymentMethodCode, processingValues\n                );\n            } else if (flow === 'token') {\n                this._processTokenFlow(\n                    providerCode, paymentOptionId, paymentMethodCode, processingValues\n                );\n            }\n        }).catch(error => {\n            if (error instanceof RPCError) {\n                this._displayErrorDialog(_t(\"Payment processing failed\"), error.data.message);\n                this._enableButton(); // The button has been disabled before initiating the flow.\n            } else {\n                return Promise.reject(error);\n            }\n        });\n    },\n\n    /**\n     * Prepare the params for the RPC to the transaction route.\n     *\n     * @private\n     * @return {object} The transaction route params.\n     */\n    _prepareTransactionRouteParams() {\n        let transactionRouteParams = {\n            'provider_id': this.paymentContext.providerId,\n            'payment_method_id': this.paymentContext.paymentMethodId ?? null,\n            'token_id': this.paymentContext.tokenId ?? null,\n            'amount': this.paymentContext['amount'] !== undefined\n                ? parseFloat(this.paymentContext['amount']) : null,\n            'flow': this.paymentContext['flow'],\n            'tokenization_requested': this.paymentContext['tokenizationRequested'],\n            'landing_route': this.paymentContext['landingRoute'],\n            'is_validation': this.paymentContext['mode'] === 'validation',\n            'access_token': this.paymentContext['accessToken'],\n            'csrf_token': odoo.csrf_token,\n        };\n        // Generic payment flows (i.e., that are not attached to a document) require extra params.\n        if (this.paymentContext['transactionRoute'] === '/payment/transaction') {\n            Object.assign(transactionRouteParams, {\n                'currency_id': this.paymentContext['currencyId']\n                    ? parseInt(this.paymentContext['currencyId']) : null,\n                'partner_id': parseInt(this.paymentContext['partnerId']),\n                'reference_prefix': this.paymentContext['referencePrefix']?.toString(),\n            });\n        }\n        return transactionRouteParams;\n    },\n\n    /**\n     * Redirect the customer by submitting the redirect form included in the processing values.\n     *\n     * @private\n     * @param {string} providerCode - The code of the selected payment option's provider.\n     * @param {number} paymentOptionId - The id of the selected payment option.\n     * @param {string} paymentMethodCode - The code of the selected payment method, if any.\n     * @param {object} processingValues - The processing values of the transaction.\n     * @return {void}\n     */\n    _processRedirectFlow(providerCode, paymentOptionId, paymentMethodCode, processingValues) {\n        // Create and configure the form element with the content rendered by the server.\n        const div = document.createElement('div');\n        div.innerHTML = processingValues['redirect_form_html'];\n        const redirectForm = div.querySelector('form');\n        redirectForm.setAttribute('id', 'o_payment_redirect_form');\n        redirectForm.setAttribute('target', '_top');  // Ensures redirections when in an iframe.\n\n        // Submit the form.\n        document.body.appendChild(redirectForm);\n        redirectForm.submit();\n    },\n\n   /**\n     * Process the provider-specific implementation of the direct payment flow.\n     *\n     * @private\n     * @param {string} providerCode - The code of the selected payment option's provider.\n     * @param {number} paymentOptionId - The id of the selected payment option.\n     * @param {string} paymentMethodCode - The code of the selected payment method, if any.\n     * @param {object} processingValues - The processing values of the transaction.\n     * @return {void}\n     */\n    _processDirectFlow(providerCode, paymentOptionId, paymentMethodCode, processingValues) {},\n\n    /**\n     * Redirect the customer to the status route.\n     *\n     * @private\n     * @param {string} providerCode - The code of the selected payment option's provider.\n     * @param {number} paymentOptionId - The id of the selected payment option.\n     * @param {string} paymentMethodCode - The code of the selected payment method, if any.\n     * @param {object} processingValues - The processing values of the transaction.\n     * @return {void}\n     */\n    _processTokenFlow(providerCode, paymentOptionId, paymentMethodCode, processingValues) {\n        // The flow is already completed as payments by tokens are immediately processed.\n        window.location = '/payment/status';\n    },\n\n    /**\n     * Archive the provided token.\n     *\n     * @private\n     * @param {number} tokenId - The id of the token whose deletion was requested.\n     * @return {void}\n     */\n    _archiveToken(tokenId) {\n        rpc('/payment/archive_token', {\n            'token_id': tokenId,\n        }).then(() => {\n            browser.location.reload();\n        }).catch(error => {\n            if (error instanceof RPCError) {\n                this._displayErrorDialog(\n                    _t(\"Cannot delete payment method\"), error.data.message\n                );\n            } else {\n                return Promise.reject(error);\n            }\n        });\n    },\n\n    // #=== GETTERS ===#\n\n    /**\n     * Determine and return the inline form of the selected payment option.\n     *\n     * @private\n     * @param {HTMLInputElement} radio - The radio button linked to the payment option.\n     * @return {Element | null} The inline form of the selected payment option, if any.\n     */\n    _getInlineForm(radio) {\n        const inlineFormContainer = radio.closest('[name=\"o_payment_option\"]');\n        return inlineFormContainer?.querySelector('[name=\"o_payment_inline_form\"]');\n    },\n\n    /**\n     * Determine and return the payment flow of the selected payment option.\n     *\n     * As some providers implement both direct payments and the payment with redirection flow, we\n     * cannot infer it from the radio button only. The radio button indicates only whether the\n     * payment option is a token. If not, the payment context is looked up to determine whether the\n     * flow is 'direct' or 'redirect'.\n     *\n     * @private\n     * @param {HTMLInputElement} radio - The radio button linked to the payment option.\n     * @return {string} The flow of the selected payment option: 'redirect', 'direct' or 'token'.\n     */\n    _getPaymentFlow(radio) {\n        // The flow is read from the payment context too in case it was forced in a custom implem.\n        if (this._getPaymentOptionType(radio) === 'token' || this.paymentContext.flow === 'token') {\n            return 'token';\n        } else if (this.paymentContext.flow === 'redirect') {\n            return 'redirect';\n        } else {\n            return 'direct';\n        }\n    },\n\n    /**\n     * Determine and return the code of the selected payment method.\n     *\n     * @private\n     * @param {HTMLElement} radio - The radio button linked to the payment method.\n     * @return {string} The code of the selected payment method.\n     */\n    _getPaymentMethodCode(radio) {\n        return radio.dataset['paymentMethodCode'];\n    },\n\n    /**\n     * Determine and return the id of the selected payment option.\n     *\n     * @private\n     * @param {HTMLElement} radio - The radio button linked to the payment option.\n     * @return {number} The id of the selected payment option.\n     */\n    _getPaymentOptionId(radio) {\n        return Number(radio.dataset['paymentOptionId']);\n    },\n\n    /**\n     * Determine and return the type of the selected payment option.\n     *\n     * @private\n     * @param {HTMLElement} radio - The radio button linked to the payment option.\n     * @return {string} The type of the selected payment option: 'token' or 'payment_method'.\n     */\n    _getPaymentOptionType(radio) {\n        return radio.dataset['paymentOptionType'];\n    },\n\n    /**\n     * Determine and return the id of the provider of the selected payment option.\n     *\n     * @private\n     * @param {HTMLElement} radio - The radio button linked to the payment option.\n     * @return {number} The id of the provider of the selected payment option.\n     */\n    _getProviderId(radio) {\n        return Number(radio.dataset['providerId']);\n    },\n\n    /**\n     * Determine and return the code of the provider of the selected payment option.\n     *\n     * @private\n     * @param {HTMLElement} radio - The radio button linked to the payment option.\n     * @return {string} The code of the provider of the selected payment option.\n     */\n    _getProviderCode(radio) {\n        return radio.dataset['providerCode'];\n    },\n\n    /**\n     * Determine and return the state of the provider of the selected payment option.\n     *\n     * @private\n     * @param {HTMLElement} radio - The radio button linked to the payment option.\n     * @return {string} The state of the provider of the selected payment option.\n     */\n    _getProviderState(radio) {\n        return radio.dataset['providerState'];\n    },\n\n});\n\nexport default publicWidget.registry.PaymentForm;\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { ConnectionLostError, rpc, RPCError } from '@web/core/network/rpc';\n\npublicWidget.registry.PaymentPostProcessing = publicWidget.Widget.extend({\n    selector: 'div[name=\"o_payment_status\"]',\n\n    timeout: 0,\n    pollCount: 0,\n\n    async start() {\n        this._poll();\n        return this._super.apply(this, arguments);\n    },\n\n    _poll() {\n        this._updateTimeout();\n        setTimeout(() => {\n            // Fetch the post-processing values from the server.\n            const self = this;\n            rpc('/payment/status/poll', {\n                'csrf_token': odoo.csrf_token,\n            }).then(postProcessingValues => {\n                let {provider_code, state, landing_route} = postProcessingValues;\n\n                // Redirect the user to the landing route if the transaction reached a final state.\n                if (self._getFinalStates(provider_code).has(state)) {\n                    window.location = landing_route;\n                } else {\n                    self._poll();\n                }\n            }).catch(error => {\n                const isRetryError = error instanceof RPCError && error.data.message === 'retry';\n                const isConnectionLostError = error instanceof ConnectionLostError;\n                if (isRetryError || isConnectionLostError) {\n                    self._poll();\n                }\n                if (!isRetryError) {\n                    throw error;\n                }\n            });\n        }, this.timeout);\n    },\n\n    _getFinalStates(providerCode) {\n        return new Set(['authorized', 'done', 'cancel', 'error']);\n    },\n\n    _updateTimeout() {\n        if (this.pollCount >= 1 && this.pollCount < 10) {\n            this.timeout = 3000;\n        }\n        if (this.pollCount >= 10 && this.pollCount < 20) {\n            this.timeout = 10000;\n        }\n        else if (this.pollCount >= 20) {\n            this.timeout = 30000;\n        }\n        this.pollCount++;\n    },\n});\n\nexport default publicWidget.registry.PaymentPostProcessing;\n", "/** @odoo-module **/\n\nimport PaymentForm from \"@payment/js/payment_form\";\n\nPaymentForm.include({\n    /**\n     * Set whether we are paying an installment before submitting.\n     *\n     * @override method from payment.payment_form\n     * @private\n     * @param {Event} ev\n     * @returns {void}\n     */\n    async _submitForm(ev) {\n        ev.stopPropagation();\n        ev.preventDefault();\n\n        const paymentDialog = this.el.closest(\"#pay_with\");\n        const chosenPaymentDetails = paymentDialog\n            ? paymentDialog.querySelector(\".o_btn_payment_tab.active\")\n            : null;\n        if (chosenPaymentDetails){\n            if (chosenPaymentDetails.id === \"o_payment_installments_tab\") {\n                this.paymentContext.amount = parseFloat(this.paymentContext.invoiceNextAmountToPay);\n            } else {\n                this.paymentContext.amount = parseFloat(this.paymentContext.invoiceAmountDue);\n            }\n        }\n        await this._super(...arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\npublicWidget.registry.PortalInvoicePagePayment = publicWidget.Widget.extend({\n    selector: \"#portal_pay\",\n\n    /**\n     * Show the payment dialog when the context parameter is set.\n     *\n     * @returns {void}\n     */\n    start() {\n        if (this.el.dataset.payment) {\n            const paymentDialog = new Modal(\"#pay_with\");\n            paymentDialog.show();\n        }\n        return this._super(...arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport {_t} from \"@web/core/l10n/translation\";\nimport {deserializeDateTime} from \"@web/core/l10n/dates\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\nconst {DateTime} = luxon;\n\npublicWidget.registry.PortalMyInvoicesPaymentList = publicWidget.Widget.extend({\n    selector: \".o_portal_my_doc_table\",\n\n    start() {\n        this._setDueDateLabel();\n        return this._super(...arguments);\n    },\n\n    _setDueDateLabel() {\n        const dueDateLabels = this.el.querySelectorAll(\".o_portal_invoice_due_date\");\n        const today = DateTime.now().startOf(\"day\");\n        dueDateLabels.forEach((label) => {\n            const dateTime = deserializeDateTime(label.getAttribute(\"datetime\")).startOf('day');\n            const diff = dateTime.diff(today).as(\"days\");\n\n            let dueDateLabel = \"\";\n\n            if (diff === 0) {\n                dueDateLabel = _t(\"due today\");\n            } else if (diff > 0) {\n                dueDateLabel = _t(\"due in %s day(s)\", Math.abs(diff).toFixed());\n            } else {\n                dueDateLabel = _t(\"%s day(s) overdue\", Math.abs(diff).toFixed());\n            }\n            // We use `.createTextNode()` to escape possible HTML in translations (XSS)\n            label.replaceChildren(document.createTextNode(dueDateLabel));\n        });\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport PortalSidebar from \"@portal/js/portal_sidebar\";\nimport { uniqueId } from \"@web/core/utils/functions\";\n\npublicWidget.registry.SalePortalSidebar = PortalSidebar.extend({\n    selector: '.o_portal_sale_sidebar',\n\n    /**\n     * @constructor\n     */\n    init: function (parent, options) {\n        this._super.apply(this, arguments);\n        this.authorizedTextTag = ['em', 'b', 'i', 'u'];\n        this.spyWatched = $('body[data-target=\".navspy\"]');\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        var def = this._super.apply(this, arguments);\n        var $spyWatcheElement = this.$el.find('[data-id=\"portal_sidebar\"]');\n        this._setElementId($spyWatcheElement);\n        // Nav Menu ScrollSpy\n        this._generateMenu();\n        // After signature, automatically open the popup for payment\n        const searchParams = new URLSearchParams(window.location.search.substring(1));\n        const payNowButton = this.$('#o_sale_portal_paynow')\n        if (searchParams.get(\"allow_payment\") === \"yes\" && payNowButton) {\n            payNowButton[0].click();\n        }\n        return def;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //---------------------------------------------------------------------------\n\n    /**\n     * create an unique id and added as a attribute of spyWatched element\n     *\n     * @private\n     * @param {string} prefix\n     * @param {Object} $el\n     *\n     */\n    _setElementId: function (prefix, $el) {\n        var id = uniqueId(prefix);\n        this.spyWatched.find($el).attr('id', id);\n        return id;\n    },\n    /**\n     * generate the new spy menu\n     *\n     * @private\n     *\n     */\n    _generateMenu: function () {\n        var self = this,\n            lastLI = false,\n            lastUL = null,\n            $bsSidenav = this.$el.find('.bs-sidenav');\n\n        $(\"#quote_content [id^=quote_header_], #quote_content [id^=quote_]\", this.spyWatched).attr(\"id\", \"\");\n        this.spyWatched.find(\"#quote_content h2, #quote_content h3\").toArray().forEach((el) => {\n            var id, text;\n            switch (el.tagName.toLowerCase()) {\n                case \"h2\":\n                    id = self._setElementId('quote_header_', el);\n                    text = self._extractText($(el));\n                    if (!text) {\n                        break;\n                    }\n                    lastLI = $(\"<li class='nav-item'>\").append($('<a class=\"nav-link p-0\" href=\"#' + id + '\"/>').text(text)).appendTo($bsSidenav);\n                    lastUL = false;\n                    break;\n                case \"h3\":\n                    id = self._setElementId('quote_', el);\n                    text = self._extractText($(el));\n                    if (!text) {\n                        break;\n                    }\n                    if (lastLI) {\n                        if (!lastUL) {\n                            lastUL = $(\"<ul class='nav flex-column'>\").appendTo(lastLI);\n                        }\n                        $(\"<li class='nav-item'>\").append($('<a class=\"nav-link p-0\" href=\"#' + id + '\"/>').text(text)).appendTo(lastUL);\n                    }\n                    break;\n            }\n            el.setAttribute('data-anchor', true);\n        });\n        this.trigger_up('widgets_start_request', {$target: $bsSidenav});\n    },\n    /**\n     * extract text of menu title for sidebar\n     *\n     * @private\n     * @param {Object} $node\n     *\n     */\n    _extractText: function ($node) {\n        var self = this;\n        var rawText = [];\n        $node.contents().toArray().forEach((el) => {\n            var current = $(el);\n            if ($.trim(current.text())) {\n                var tagName = current.prop(\"tagName\");\n                if (\n                    typeof tagName === \"undefined\" ||\n                    (typeof tagName !== \"undefined\" &&\n                        self.authorizedTextTag.includes(tagName.toLowerCase()))\n                ) {\n                    rawText.push($.trim(current.text()));\n                }\n            }\n        });\n        return rawText.join(' ');\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\npublicWidget.registry.PortalPrepayment = publicWidget.Widget.extend({\n    selector: '.o_portal_sale_sidebar',\n    events: Object.assign({}, publicWidget.Widget.prototype.events, {\n        'click button[name=\"o_sale_portal_amount_prepayment_button\"]': '_onClickAmountPrepaymentButton',\n        'click button[name=\"o_sale_portal_amount_total_button\"]': '_onClickAmountTotalButton',\n    }),\n\n    start: async function () {\n        this.AmountTotalButton = document.querySelector(\n            'button[name=\"o_sale_portal_amount_total_button\"]'\n        );\n        this.AmountPrepaymentButton = document.querySelector(\n            'button[name=\"o_sale_portal_amount_prepayment_button\"]'\n        );\n\n        if (!this.AmountTotalButton) {\n            // Button not available in dom => confirmed SO or partial payment not enabled on this SO\n            // this widget has nothing to manage\n            return;\n        }\n\n        const params = new URLSearchParams(window.location.search);\n        const isPartialPayment = params.has('downpayment') ? params.get('downpayment') === 'true': true;\n        const showPaymentModal = params.get('showPaymentModal') === 'true';\n\n        // Prepare the modal to show if the down payment amount is selected or not.\n        if (isPartialPayment) {\n            this._onClickAmountPrepaymentButton(false);\n        } else {\n            this._onClickAmountTotalButton(false);\n        }\n\n        // When updating the amount re-open the modal.\n        if (showPaymentModal) {\n            const payNowButton = this.$('#o_sale_portal_paynow')[0];\n            payNowButton && payNowButton.click();\n        }\n    },\n\n    _onClickAmountPrepaymentButton: function (doReload=true) {\n        this.AmountTotalButton?.classList.remove('active');\n        this.AmountPrepaymentButton?.classList.add('active');\n\n        if (doReload) {\n            this._reloadAmount(true);\n        } else {\n            this.$('span[id=\"o_sale_portal_use_amount_total\"]').hide();\n            this.$('span[id=\"o_sale_portal_use_amount_prepayment\"]').show();\n        }\n    },\n\n    _onClickAmountTotalButton: function(doReload=true) {\n        this.AmountPrepaymentButton?.classList.remove('active');\n        this.AmountTotalButton?.classList.add('active');\n\n        if (doReload) {\n            this._reloadAmount(false);\n        } else {\n            this.$('span[id=\"o_sale_portal_use_amount_total\"]').show();\n            this.$('span[id=\"o_sale_portal_use_amount_prepayment\"]').hide();\n        }\n    },\n\n    _reloadAmount: function (partialPayment) {\n        const searchParams = new URLSearchParams(window.location.search);\n\n        if (partialPayment) {\n            searchParams.set('downpayment', true);\n        } else {\n            searchParams.set('downpayment', false);\n        }\n        searchParams.set('showPaymentModal', true);\n\n        window.location.search = searchParams.toString();\n    },\n});\nexport default publicWidget.registry.PortalPrepayment;\n", "/** @odoo-module */\n\nimport { PortalHomeCounters } from '@portal/js/portal';\n\nPortalHomeCounters.include({\n    /**\n     * @override\n     */\n    _getCountersAlwaysDisplayed() {\n        return this._super(...arguments).concat(['order_count']);\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { rpc } from \"@web/core/network/rpc\";\n\npublicWidget.registry.SaleUpdateLineButton = publicWidget.Widget.extend({\n    selector: '.o_portal_sale_sidebar',\n    events: {\n        'click a.js_update_line_json': '_onClickOptionQuantityButton',\n        'click a.js_add_optional_products': '_onClickAddOptionalProduct',\n        'change .js_quantity': '_onChangeOptionQuantity',\n    },\n\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n        this.orderDetail = this.$el.find('table#sales_order_table').data();\n    },\n\n    /**\n     * Calls the route to get updated values of the line and order\n     * when the quantity of a product has changed\n     *\n     * @private\n     * @param {integer} order_id\n     * @param {Object} params\n     * @return {Deferred}\n     */\n     _callUpdateLineRoute(order_id, params) {\n        return rpc(\"/my/orders/\" + order_id + \"/update_line_dict\", params);\n    },\n\n    /**\n     * Refresh the UI of the order details\n     *\n     * @private\n     * @param {Object} data: contains order html details\n     */\n    _refreshOrderUI(data){\n        window.location.reload();\n    },\n\n    /**\n     * Process the change in line quantity\n     *\n     * @private\n     * @param {Event} ev\n     */\n    async _onChangeOptionQuantity(ev) {\n        ev.preventDefault();\n        let self = this,\n            $target = $(ev.currentTarget),\n            quantity = parseInt($target.val());\n\n        const result = await this._callUpdateLineRoute(self.orderDetail.orderId, {\n            'line_id': $target.data('lineId'),\n            'input_quantity': quantity >= 0 ? quantity : false,\n            'access_token': self.orderDetail.token\n        });\n        this._refreshOrderUI(result);\n    },\n\n    /**\n     * Reacts to the click on the -/+ buttons\n     *\n     * @private\n     * @param {Event} ev\n     */\n    async _onClickOptionQuantityButton(ev) {\n        ev.preventDefault();\n        let self = this,\n            $target = $(ev.currentTarget);\n\n        const result = await this._callUpdateLineRoute(self.orderDetail.orderId, {\n            'line_id': $target.data('lineId'),\n            'remove': $target.data('remove'),\n            'unlink': $target.data('unlink'),\n            'access_token': self.orderDetail.token\n        });\n        this._refreshOrderUI(result);\n    },\n\n    /**\n     * Triggered when optional product added to order from portal.\n     *\n     * @private\n     * @param {Event} ev\n     */\n     _onClickAddOptionalProduct(ev) {\n        ev.preventDefault();\n        let self = this,\n            $target = $(ev.currentTarget);\n\n        // to avoid double click on link with href.\n        $target.css('pointer-events', 'none');\n\n        rpc(\n            \"/my/orders/\" + self.orderDetail.orderId + \"/add_option/\" + $target.data('optionId'),\n            {access_token: self.orderDetail.token}\n        ).then((data) => {\n            this._refreshOrderUI(data);\n        });\n    },\n\n});\n", "/** @odoo-module **/\n\nimport { session } from \"@web/session\";\nimport { loadJS } from \"@web/core/assets\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ReCaptcha {\n    /**\n     * @override\n     */\n    constructor() {\n        this._publicKey = session.recaptcha_public_key;\n    }\n    /**\n     * Loads the recaptcha libraries.\n     *\n     * @returns {Promise|boolean} promise if libs are loading else false if the reCaptcha key is empty.\n     */\n    loadLibs() {\n        if (this._publicKey) {\n            this._recaptchaReady = loadJS(`https://www.recaptcha.net/recaptcha/api.js?render=${encodeURIComponent(this._publicKey)}`)\n                .then(() => new Promise(resolve => window.grecaptcha.ready(() => resolve())));\n            return this._recaptchaReady.then(() => !!document.querySelector('.grecaptcha-badge'));\n        }\n        return false;\n    }\n    /**\n     * Returns an object with the token if reCaptcha call succeeds\n     * If no key is set an object with a message is returned\n     * If an error occurred an object with the error message is returned\n     *\n     * @param {string} action\n     * @returns {Promise|Object}\n     */\n    async getToken(action) {\n        if (!this._publicKey) {\n            return {\n                message: _t(\"No recaptcha site key set.\"),\n            };\n        }\n        await this._recaptchaReady;\n        try {\n            return {\n                token: await window.grecaptcha.execute(this._publicKey, {action: action})\n            };\n        } catch {\n            return {\n                error: _t(\"The recaptcha site key is invalid.\"),\n            };\n        }\n    }\n}\n\nexport default {\n    ReCaptcha: ReCaptcha,\n};\n", "/** @odoo-module **/\n\n/**\n    This code has been more that widely inspired by easyZoom library.\n\n    Copyright 2013 Matt Hinchliffe\n\n    Permission is hereby granted, free of charge, to any person obtaining\n    a copy of this software and associated documentation files (the\n    \"Software\"), to deal in the Software without restriction, including\n    without limitation the rights to use, copy, modify, merge, publish,\n    distribute, sublicense, and/or sell copies of the Software, and to\n    permit persons to whom the Software is furnished to do so, subject to\n    the following conditions:\n\n    The above copyright notice and this permission notice shall be\n    included in all copies or substantial portions of the Software.\n**/\n\nvar dw, dh, rw, rh, lx, ly;\n\nvar defaults = {\n\n    // Attribute to retrieve the zoom image URL from.\n    linkTag: 'a',\n    linkAttribute: 'data-zoom-image',\n\n    // event to trigger zoom\n    event: 'click', //or mouseenter\n\n    // Timer before trigger zoom\n    timer: 0,\n\n    // Prevent clicks on the zoom image link.\n    preventClicks: true,\n\n    // disable on mobile\n    disabledOnMobile: true,\n\n    // Callback function to execute before the flyout is displayed.\n    beforeShow: $.noop,\n\n    // Callback function to execute before the flyout is removed.\n    beforeHide: $.noop,\n\n    // Callback function to execute when the flyout is displayed.\n    onShow: $.noop,\n\n    // Callback function to execute when the flyout is removed.\n    onHide: $.noop,\n\n    // Callback function to execute when the cursor is moved while over the image.\n    onMove: $.noop,\n\n    // Callback function to execute when the flyout is attached to the target.\n    beforeAttach: $.noop\n\n};\n\n/**\n * ZoomOdoo\n * @constructor\n * @param {Object} target\n * @param {Object} options (Optional)\n */\nfunction ZoomOdoo(target, options) {\n    this.$target = $(target);\n    this.opts = $.extend({}, defaults, options, this.$target.data());\n\n    if (this.isOpen === undefined) {\n        this._init();\n    }\n}\n\n/**\n * Init\n * @private\n */\nZoomOdoo.prototype._init = function () {\n    if (window.outerWidth > 467 || !this.opts.disabledOnMobile) {\n        this.$link  = this.$target.find(this.opts.linkTag).length && this.$target.find(this.opts.linkTag) || this.$target;\n        this.$image  = this.$target.find('img').length && this.$target.find('img') || this.$target;\n        this.$flyout = $('<div class=\"zoomodoo-flyout\" />');\n\n        var $attach = this.$target;\n        if (this.opts.attach !== undefined && this.$target.closest(this.opts.attach).length) {\n            $attach = this.$target.closest(this.opts.attach);\n        }\n        $attach.parent().on('mousemove.zoomodoo touchmove.zoomodoo', $.proxy(this._onMove, this));\n        $attach.parent().on('mouseleave.zoomodoo touchend.zoomodoo', $.proxy(this._onLeave, this));\n        this.$target.on(this.opts.event + '.zoomodoo touchstart.zoomodoo', $.proxy(this._onEnter, this));\n\n        if (this.opts.preventClicks) {\n            this.$target.on('click.zoomodoo', function (e) { e.preventDefault(); });\n        } else {\n            var self = this;\n            this.$target.on('click.zoomodoo', function () { self.hide(); self.$target.unbind(); });\n        }\n    }\n};\n\n/**\n * Show\n * @param {MouseEvent|TouchEvent} e\n * @param {Boolean} testMouseOver (Optional)\n */\nZoomOdoo.prototype.show = function (e, testMouseOver) {\n    var w1, h1, w2, h2;\n    var self = this;\n\n    if (this.opts.beforeShow.call(this) === false) return;\n\n    if (!this.isReady) {\n        return this._loadImage(this.$link.attr(this.opts.linkAttribute), function () {\n            if (self.isMouseOver || !testMouseOver) {\n                self.show(e);\n            }\n        });\n    }\n\n    var $attach = this.$target;\n    if (this.opts.attach !== undefined && this.$target.closest(this.opts.attach).length) {\n        $attach = this.$target.closest(this.opts.attach);\n    }\n\n    // Prevents having multiple zoom flyouts\n    $attach.parent().find('.zoomodoo-flyout').remove();\n    this.$flyout.removeAttr('style');\n    $attach.parent().append(this.$flyout);\n\n    if (this.opts.attachToTarget) {\n        this.opts.beforeAttach.call(this);\n\n        // Be sure that the flyout is at top 0, left 0 to ensure correct computation\n        // e.g. employees kanban on dashboard\n        this.$flyout.css('position', 'fixed');\n        var flyoutOffset = this.$flyout.offset();\n        if (flyoutOffset.left > 0) {\n            var flyoutLeft = parseFloat(this.$flyout.css('left').replace('px',''));\n            this.$flyout.css('left', flyoutLeft - flyoutOffset.left + 'px');\n        }\n        if (flyoutOffset.top > 0) {\n            var flyoutTop = parseFloat(this.$flyout.css('top').replace('px',''));\n            this.$flyout.css('top', flyoutTop - flyoutOffset.top + 'px');\n        }\n\n        if(this.$zoom.height() < this.$flyout.height()) {\n             this.$flyout.css('height', this.$zoom.height() + 'px');\n        }\n        if(this.$zoom.width() < this.$flyout.width()) {\n             this.$flyout.css('width', this.$zoom.width() + 'px');\n        }\n\n        var offset = this.$target.offset();\n        var left = offset.left - this.$flyout.width();\n        var top = offset.top;\n\n        // Position the zoom on the right side of the target\n        // if there's not enough room on the left\n        if(left < 0) {\n            if(offset.left < ($(document).width() / 2)) {\n                left = offset.left + this.$target.width();\n            } else {\n                left = 0;\n            }\n        }\n\n        // Prevents the flyout to overflow\n        if(left + this.$flyout.width() > $(document).width()) {\n            this.$flyout.css('width',  $(document).width() - left + 'px');\n        } else if(left === 0) { // Limit the max width if displayed on the left\n            this.$flyout.css('width', offset.left + 'px');\n        }\n\n        // Prevents the zoom to be displayed outside the current viewport\n        if((top + this.$flyout.height()) > $(document).height()) {\n            top = $(document).height() - this.$flyout.height();\n        }\n\n        this.$flyout.css('transform', 'translate3d(' + left + 'px, ' + top + 'px, 0px)');\n    } else {\n        // Computing flyout max-width depending to the available space on the right to avoid overflow-x issues\n        // e.g. width too high so a right zoomed element is not visible (need to scroll on x axis)\n        var rightAvailableSpace = document.body.clientWidth - this.$flyout[0].getBoundingClientRect().left;\n        this.$flyout.css('max-width', rightAvailableSpace);\n    }\n\n    w1 = this.$target[0].offsetWidth;\n    h1 = this.$target[0].offsetHeight;\n\n    w2 = this.$flyout.width();\n    h2 = this.$flyout.height();\n\n    dw = this.$zoom.width() - w2;\n    dh = this.$zoom.height() - h2;\n\n    // For the case where the zoom image is actually smaller than\n    // the flyout.\n    if (dw < 0) dw = 0;\n    if (dh < 0) dh = 0;\n\n    rw = dw / w1;\n    rh = dh / h1;\n\n    this.isOpen = true;\n\n    this.opts.onShow.call(this);\n\n    if (e) {\n        this._move(e);\n    }\n};\n\n/**\n * On enter\n * @private\n * @param {Event} e\n */\nZoomOdoo.prototype._onEnter = function (e) {\n    var self = this;\n    var touches = e.originalEvent.touches;\n    e.preventDefault();\n    this.isMouseOver = true;\n\n    setTimeout(function () {\n        if (self.isMouseOver && (!touches || touches.length === 1)) {\n            self.show(e, true);\n        }\n      }, this.opts.timer);\n\n};\n\n/**\n * On move\n * @private\n * @param {Event} e\n */\nZoomOdoo.prototype._onMove = function (e) {\n    if (!this.isOpen) return;\n\n    e.preventDefault();\n    this._move(e);\n};\n\n/**\n * On leave\n * @private\n */\nZoomOdoo.prototype._onLeave = function () {\n    this.isMouseOver = false;\n    if (this.isOpen) {\n        this.hide();\n    }\n};\n\n/**\n * On load\n * @private\n * @param {Event} e\n */\nZoomOdoo.prototype._onLoad = function (e) {\n    // IE may fire a load event even on error so test the image dimensions\n    if (!e.currentTarget.width) return;\n\n    this.isReady = true;\n\n    this.$flyout.html(this.$zoom);\n\n    if (e.data.call) {\n        e.data();\n    }\n};\n\n/**\n * Load image\n * @private\n * @param {String} href\n * @param {Function} callback\n */\nZoomOdoo.prototype._loadImage = function (href, callback) {\n    var zoom = new Image();\n\n    this.$zoom = $(zoom).on('load', callback, $.proxy(this._onLoad, this));\n\n    zoom.style.position = 'absolute';\n    zoom.src = href;\n};\n\n/**\n * Move\n * @private\n * @param {Event} e\n */\nZoomOdoo.prototype._move = function (e) {\n    if (e.type.indexOf('touch') === 0) {\n        var touchlist = e.touches || e.originalEvent.touches;\n        lx = touchlist[0].pageX;\n        ly = touchlist[0].pageY;\n    } else {\n        lx = e.pageX || lx;\n        ly = e.pageY || ly;\n    }\n\n    var offset  = this.$target.offset();\n    var pt = ly - offset.top;\n    var pl = lx - offset.left;\n    var xt = Math.ceil(pt * rh);\n    var xl = Math.ceil(pl * rw);\n\n    // Close if outside\n    if (!this.opts.attachToTarget && (xl < 0 || xt < 0 || xl > dw || xt > dh || lx > (offset.left + this.$target.outerWidth()))) {\n        this.hide();\n    } else {\n        var top = xt * -1;\n        var left = xl * -1;\n\n        this.$zoom.css({\n            top: top,\n            left: left\n        });\n\n        this.opts.onMove.call(this, top, left);\n    }\n\n};\n\n/**\n * Hide\n */\nZoomOdoo.prototype.hide = function () {\n    if (!this.isOpen) return;\n    if (this.opts.beforeHide.call(this) === false) return;\n\n    this.$flyout.detach();\n    this.isOpen = false;\n\n    this.opts.onHide.call(this);\n};\n\n// jQuery plugin wrapper\n$.fn.zoomOdoo = function (options) {\n    return this.each(function () {\n        var api = $.data(this, 'zoomOdoo');\n\n        if (!api) {\n            $.data(this, 'zoomOdoo', new ZoomOdoo(this, options));\n        } else if (api.isOpen === undefined) {\n            api._init();\n        }\n    });\n};\n", "/**\n * Grep `_detectNavbar`: the dynamic navbar's dropdown positioning was activated\n * to prevent sub-menus overflow. This positioning will use the default BS\n * offsets to position sub-menus leading to a small gap that hides them when\n * hovered (on \"Hover\" mode). The goal here is to prevent this offset when the\n * target is inside a navbar.\n */\nconst bsGetOffsetFunction = Dropdown.prototype._getOffset;\nDropdown.prototype._getOffset = function () {\n    const offset = bsGetOffsetFunction.apply(this, arguments);\n    if (this._element.closest(\".o_hoverable_dropdown .navbar\")) {\n        return [offset[0], 0];\n    }\n    return offset;\n};\n", "/** @odoo-module **/\n\nimport { intersection } from \"@web/core/utils/arrays\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { App, Component } from \"@odoo/owl\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { UrlAutoComplete } from \"@website/components/autocomplete_with_pages/url_autocomplete\";\n\n/**\n * Allows to load anchors from a page.\n *\n * @param {string} url\n * @param {Node} body the editable for which to recover anchors\n * @returns {Deferred<string[]>}\n */\nfunction loadAnchors(url, body) {\n    return new Promise(function (resolve, reject) {\n        if (url === window.location.pathname || url[0] === '#') {\n            resolve(body ? body : document.body.outerHTML);\n        } else if (url.length && !url.startsWith(\"http\")) {\n            $.get(window.location.origin + url).then(resolve, reject);\n        } else { // avoid useless query\n            resolve();\n        }\n    }).then(function (response) {\n        const anchors = $(response).find('[id][data-anchor=true], .modal[id][data-display=\"onClick\"]').toArray().map((el) => {\n            return '#' + el.id;\n        });\n        // Always suggest the top and the bottom of the page as internal link\n        // anchor even if the header and the footer are not in the DOM. Indeed,\n        // the \"scrollTo\" function handles the scroll towards those elements\n        // even when they are not in the DOM.\n        if (!anchors.includes('#top')) {\n            anchors.unshift('#top');\n        }\n        if (!anchors.includes('#bottom')) {\n            anchors.push('#bottom');\n        }\n        return anchors;\n    }).catch(error => {\n        console.debug(error);\n        return [];\n    });\n}\n\n/**\n * Allows the given input to propose existing website URLs.\n *\n * @param {HTMLInputElement} input\n */\nfunction autocompleteWithPages(input, options= {}) {\n    const owlApp = new App(UrlAutoComplete, {\n        env: Component.env,\n        dev: Component.env.debug,\n        getTemplate,\n        props: {\n            options,\n            loadAnchors,\n            targetDropdown: input,\n        },\n        translatableAttributes: [\"data-tooltip\"],\n        translateFn: _t,\n    });\n\n    const container = document.createElement(\"div\");\n    container.classList.add(\"ui-widget\", \"ui-autocomplete\", \"ui-widget-content\", \"border-0\");\n    document.body.appendChild(container);\n    owlApp.mount(container)\n    return () => {\n        owlApp.destroy();\n        container.remove();\n    }\n}\n\n/**\n * @param {jQuery} $element\n * @param {jQuery} [$excluded]\n */\nfunction onceAllImagesLoaded($element, $excluded) {\n    var defs = Array.from($element.find(\"img\").addBack(\"img\")).map((img) => {\n        if (img.complete || $excluded && ($excluded.is(img) || $excluded.has(img).length)) {\n            return; // Already loaded\n        }\n        var def = new Promise(function (resolve, reject) {\n            $(img).one('load', function () {\n                resolve();\n            });\n        });\n        return def;\n    });\n    return Promise.all(defs);\n}\n\n/**\n * @deprecated\n * @todo create Dialog.prompt instead of this\n */\nfunction prompt(options, _qweb) {\n    /**\n     * A bootstrapped version of prompt() albeit asynchronous\n     * This was built to quickly prompt the user with a single field.\n     * For anything more complex, please use editor.Dialog class\n     *\n     * Usage Ex:\n     *\n     * website.prompt(\"What... is your quest?\").then(function (answer) {\n     *     arthur.reply(answer || \"To seek the Holy Grail.\");\n     * });\n     *\n     * website.prompt({\n     *     select: \"Please choose your destiny\",\n     *     init: function () {\n     *         return [ [0, \"Sub-Zero\"], [1, \"Robo-Ky\"] ];\n     *     }\n     * }).then(function (answer) {\n     *     mame_station.loadCharacter(answer);\n     * });\n     *\n     * @param {Object|String} options A set of options used to configure the prompt or the text field name if string\n     * @param {String} [options.window_title=''] title of the prompt modal\n     * @param {String} [options.input] tell the modal to use an input text field, the given value will be the field title\n     * @param {String} [options.textarea] tell the modal to use a textarea field, the given value will be the field title\n     * @param {String} [options.select] tell the modal to use a select box, the given value will be the field title\n     * @param {Object} [options.default=''] default value of the field\n     * @param {Function} [options.init] optional function that takes the `field` (enhanced with a fillWith() method) and the `dialog` as parameters [can return a promise]\n     */\n    if (typeof options === 'string') {\n        options = {\n            text: options\n        };\n    }\n    if (typeof _qweb === \"undefined\") {\n        _qweb = 'website.prompt';\n    }\n    options = Object.assign({\n        window_title: '',\n        field_name: '',\n        'default': '', // dict notation for IE<9\n        init: function () {},\n        btn_primary_title: _t('Create'),\n        btn_secondary_title: _t('Cancel'),\n    }, options || {});\n\n    var type = intersection(Object.keys(options), ['input', 'textarea', 'select']);\n    type = type.length ? type[0] : 'input';\n    options.field_type = type;\n    options.field_name = options.field_name || options[type];\n\n    var def = new Promise(function (resolve, reject) {\n        var dialog = $(renderToElement(_qweb, options)).appendTo('body');\n        options.$dialog = dialog;\n        var field = dialog.find(options.field_type).first();\n        field.val(options['default']); // dict notation for IE<9\n        field.fillWith = function (data) {\n            if (field.is('select')) {\n                var select = field[0];\n                data.forEach(function (item) {\n                    select.options[select.options.length] = new window.Option(item[1], item[0]);\n                });\n            } else {\n                field.val(data);\n            }\n        };\n        var init = options.init(field, dialog);\n        Promise.resolve(init).then(function (fill) {\n            if (fill) {\n                field.fillWith(fill);\n            }\n            dialog.modal('show');\n            field.focus();\n            dialog.on('click', '.btn-primary', function () {\n                var backdrop = $('.modal-backdrop');\n                resolve({ val: field.val(), field: field, dialog: dialog });\n                dialog.modal('hide').remove();\n                    backdrop.remove();\n            });\n        });\n        dialog.on('hidden.bs.modal', function () {\n                var backdrop = $('.modal-backdrop');\n            reject();\n            dialog.remove();\n                backdrop.remove();\n        });\n        if (field.is('input[type=\"text\"], select')) {\n            field.keypress(function (e) {\n                if (e.key === \"Enter\") {\n                    e.preventDefault();\n                    dialog.find('.btn-primary').trigger('click');\n                }\n            });\n        }\n    });\n\n    return def;\n}\n\nfunction websiteDomain(self) {\n    var websiteID;\n    self.trigger_up('context_get', {\n        callback: function (ctx) {\n            websiteID = ctx['website_id'];\n        },\n    });\n    return ['|', ['website_id', '=', false], ['website_id', '=', websiteID]];\n}\n\n/**\n * Checks if the 2 given URLs are the same, to prevent redirecting uselessly\n * from one to another.\n * It will consider naked URL and `www` URL as the same URL.\n * It will consider `https` URL `http` URL as the same URL.\n *\n * @param {string} url1\n * @param {string} url2\n * @returns {Boolean}\n */\nfunction isHTTPSorNakedDomainRedirection(url1, url2) {\n    try {\n        url1 = new URL(url1).host;\n        url2 = new URL(url2).host;\n    } catch {\n        // Incorrect URL, `false` URL..\n        return false;\n    }\n    return url1 === url2 ||\n           url1.replace(/^www\\./, '') === url2.replace(/^www\\./, '');\n}\n\nfunction sendRequest(route, params) {\n    function _addInput(form, name, value) {\n        let param = document.createElement('input');\n        param.setAttribute('type', 'hidden');\n        param.setAttribute('name', name);\n        param.setAttribute('value', value);\n        form.appendChild(param);\n    }\n\n    let form = document.createElement('form');\n    form.setAttribute('action', route);\n    form.setAttribute('method', params.method || 'POST');\n    // This is an exception for the 404 page create page button, in backend we\n    // want to open the response in the top window not in the iframe.\n    if (params.forceTopWindow) {\n        form.setAttribute('target', '_top');\n    }\n\n    if (odoo.csrf_token) {\n        _addInput(form, 'csrf_token', odoo.csrf_token);\n    }\n\n    for (const key in params) {\n        const value = params[key];\n        if (Array.isArray(value) && value.length) {\n            for (const val of value) {\n                _addInput(form, key, val);\n            }\n        } else {\n            _addInput(form, key, value);\n        }\n    }\n\n    document.body.appendChild(form);\n    form.submit();\n}\n\n/**\n * Converts a base64 SVG into a base64 PNG.\n *\n * @param {string|HTMLImageElement} src - an URL to a SVG or a *loaded* image\n *      with such an URL. This allows the call to potentially be a bit more\n *      efficient in that second case.\n * @returns {Promise<string>} a base64 PNG (as result of a Promise)\n */\nexport async function svgToPNG(src) {\n    return _exportToPNG(src, \"svg+xml\");\n}\n\n/**\n * Converts a base64 WEBP into a base64 PNG.\n *\n * @param {string|HTMLImageElement} src - an URL to a WEBP or a *loaded* image\n *     with such an URL. This allows the call to potentially be a bit more\n *     efficient in that second case.\n * @returns {Promise<string>} a base64 PNG (as result of a Promise)\n */\nexport async function webpToPNG(src) {\n    return _exportToPNG(src, \"webp\");\n}\n\n/**\n * Converts a formatted base64 image into a base64 PNG.\n *\n * @private\n * @param {string|HTMLImageElement} src - an URL to a image or a *loaded* image\n *     with such an URL. This allows the call to potentially be a bit more\n *     efficient in that second case.\n * @param {string} format - the format of the image\n * @returns {Promise<string>} a base64 PNG (as result of a Promise)\n */\nasync function _exportToPNG(src, format) {\n    function checkImg(imgEl) {\n        // Firefox does not support drawing SVG to canvas unless it has width\n        // and height attributes set on the root <svg>.\n        return (imgEl.naturalHeight !== 0);\n    }\n    function toPNGViaCanvas(imgEl) {\n        const canvas = document.createElement('canvas');\n        canvas.width = imgEl.width;\n        canvas.height = imgEl.height;\n        canvas.getContext('2d').drawImage(imgEl, 0, 0);\n        return canvas.toDataURL('image/png');\n    }\n\n    // In case we receive a loaded image and that this image is not problematic,\n    // we can convert it to PNG directly.\n    if (src instanceof HTMLImageElement) {\n        const loadedImgEl = src;\n        if (checkImg(loadedImgEl)) {\n            return toPNGViaCanvas(loadedImgEl);\n        }\n        src = loadedImgEl.src;\n    }\n\n    // At this point, we either did not receive a loaded image or the received\n    // loaded image is problematic => we have to do some asynchronous code.\n    return new Promise(resolve => {\n        const imgEl = new Image();\n        imgEl.onload = () => {\n            if (format !== \"svg+xml\" || checkImg(imgEl)) {\n                resolve(imgEl);\n                return;\n            }\n\n            // Set arbitrary height on image and attach it to the DOM to force\n            // width computation.\n            imgEl.height = 1000;\n            imgEl.style.opacity = 0;\n            document.body.appendChild(imgEl);\n\n            const request = new XMLHttpRequest();\n            request.open('GET', imgEl.src, true);\n            request.onload = () => {\n                // Convert the data URI to a SVG element\n                const parser = new DOMParser();\n                const result = parser.parseFromString(request.responseText, 'text/xml');\n                const svgEl = result.getElementsByTagName(\"svg\")[0];\n\n                // Add the attributes Firefox needs and remove the image from\n                // the DOM.\n                svgEl.setAttribute('width', imgEl.width);\n                svgEl.setAttribute('height', imgEl.height);\n                imgEl.remove();\n\n                // Convert the SVG element to a data URI\n                const svg64 = btoa(new XMLSerializer().serializeToString(svgEl));\n                const finalImg = new Image();\n                finalImg.onload = () => {\n                    resolve(finalImg);\n                };\n                finalImg.src = `data:image/svg+xml;base64,${svg64}`;\n            };\n            request.send();\n        };\n        imgEl.src = src;\n    }).then(loadedImgEl => toPNGViaCanvas(loadedImgEl));\n}\n\n/**\n * Bootstraps an \"empty\" Google Maps iframe.\n *\n * @returns {HTMLIframeElement}\n */\nexport function generateGMapIframe() {\n    const iframeEl = document.createElement('iframe');\n    iframeEl.classList.add('s_map_embedded', 'o_not_editable');\n    iframeEl.setAttribute('width', '100%');\n    iframeEl.setAttribute('height', '100%');\n    iframeEl.setAttribute('frameborder', '0');\n    iframeEl.setAttribute('scrolling', 'no');\n    iframeEl.setAttribute('marginheight', '0');\n    iframeEl.setAttribute('marginwidth', '0');\n    iframeEl.setAttribute('src', 'about:blank');\n    iframeEl.setAttribute('aria-label', _t(\"Map\"));\n    return iframeEl;\n}\n\n/**\n * Generates a Google Maps URL based on the given parameter.\n *\n * @param {DOMStringMap} dataset\n * @returns {string} a Google Maps URL\n */\nexport function generateGMapLink(dataset) {\n    return 'https://maps.google.com/maps?q=' + encodeURIComponent(dataset.mapAddress)\n        + '&t=' + encodeURIComponent(dataset.mapType)\n        + '&z=' + encodeURIComponent(dataset.mapZoom)\n        + '&ie=UTF8&iwloc=&output=embed';\n}\n\n/**\n * Checks if the edited content is currently previewed as in a mobile device.\n *\n * @param {Object} self - context object (\"this\")\n * @returns {boolean}\n */\nfunction isMobile(self) {\n    let isMobile;\n    self.trigger_up(\"service_context_get\", {\n        callback: (ctx) => {\n            isMobile = ctx[\"isMobile\"];\n        },\n    });\n\n    return isMobile;\n}\n\n/**\n * Returns the parsed data coming from the data-for element for the given form.\n *\n * @param {string} formId\n * @param {HTMLElement} parentEl\n * @returns {Object|undefined} the parsed data\n */\nfunction getParsedDataFor(formId, parentEl) {\n    const dataForEl = parentEl.querySelector(`[data-for='${formId}']`);\n    if (!dataForEl) {\n        return;\n    }\n    return JSON.parse(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 * Deep clones children or parses a string into elements, with or without\n * <script> elements.\n *\n * @param {DocumentFragment|HTMLElement|String} content\n * @param {Boolean} [keepScripts=false] - whether to keep script tags or not.\n * @returns {DocumentFragment}\n */\nexport function cloneContentEls(content, keepScripts = false) {\n    let copyFragment;\n    if (typeof content === \"string\") {\n        copyFragment = new Range().createContextualFragment(content);\n    } else {\n        copyFragment = new DocumentFragment();\n        const els = [...content.children].map(el => el.cloneNode(true));\n        copyFragment.append(...els);\n    }\n    if (!keepScripts) {\n        copyFragment.querySelectorAll(\"script\").forEach(scriptEl => scriptEl.remove());\n    }\n    return copyFragment;\n}\n\n/**\n * Checks SEO data and notifies if either the page title or description is not\n * set.\n *\n * @param {Object} seo_data - The SEO data to check.\n * @param {Component} OptimizeSEODialog - Dialog to be displayed\n * @param {Object} services - Services object which will be used to display\n * notifications and dialog.\n */\nexport function checkAndNotifySEO(seo_data, OptimizeSEODialog, services) {\n    if (seo_data) {\n        let message;\n        if (!seo_data.website_meta_title) {\n            message = _t(\"Page title not set.\");\n        } else if (!seo_data.website_meta_description) {\n            message = _t(\"Page description not set.\");\n        }\n        if (message) {\n            services.notification.add(message, {\n                type: \"warning\",\n                sticky: false,\n                buttons: [\n                    {\n                        name: _t(\"Optimize SEO\"),\n                        onClick: () => {\n                            services.dialog.add(OptimizeSEODialog);\n                        },\n                    },\n                ],\n            });\n        }\n    }\n}\n\nexport default {\n    loadAnchors: loadAnchors,\n    autocompleteWithPages: autocompleteWithPages,\n    onceAllImagesLoaded: onceAllImagesLoaded,\n    prompt: prompt,\n    sendRequest: sendRequest,\n    websiteDomain: websiteDomain,\n    isHTTPSorNakedDomainRedirection: isHTTPSorNakedDomainRedirection,\n    svgToPNG: svgToPNG,\n    webpToPNG: webpToPNG,\n    generateGMapIframe: generateGMapIframe,\n    generateGMapLink: generateGMapLink,\n    isMobile: isMobile,\n    getParsedDataFor: getParsedDataFor,\n    cloneContentEls: cloneContentEls,\n    checkAndNotifySEO: checkAndNotifySEO,\n};\n", "/** @odoo-module **/\n\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { useEffect } from \"@odoo/owl\";\n\nexport class AutoCompleteWithPages extends AutoComplete {\n    static props = {\n        ...AutoComplete.props,\n        targetDropdown: { type: HTMLElement },\n        dropdownClass: { type: String, optional: true },\n        dropdownOptions: { type: Object, optional: true },\n    };\n    static template = \"website.AutoCompleteWithPages\";\n\n    setup() {\n        super.setup();\n        useEffect(\n            (input, inputRef) => {\n                if (inputRef) {\n                    inputRef.value = input.value;\n                }\n                const targetBlur = this.onInputBlur.bind(this);\n                const targetClick = this._syncInputClick.bind(this);\n                const targetChange = this.onInputChange.bind(this);\n                const targetInput = this._syncInputValue.bind(this);\n                const targetKeydown = this.onInputKeydown.bind(this);\n                const targetFocus = this.onInputFocus.bind(this);\n                input.addEventListener(\"blur\", targetBlur);\n                input.addEventListener(\"click\", targetClick);\n                input.addEventListener(\"change\", targetChange);\n                input.addEventListener(\"input\", targetInput);\n                input.addEventListener(\"keydown\", targetKeydown);\n                input.addEventListener(\"focus\", targetFocus);\n                return () => {\n                    input.removeEventListener(\"blur\", targetBlur);\n                    input.removeEventListener(\"click\", targetClick);\n                    input.removeEventListener(\"change\", targetChange);\n                    input.removeEventListener(\"input\", targetInput);\n                    input.removeEventListener(\"keydown\", targetKeydown);\n                    input.removeEventListener(\"focus\", targetFocus);\n                };\n            },\n            () => [this.targetDropdown, this.inputRef.el]\n        );\n    }\n\n    get dropdownOptions() {\n        if (this.props.dropdownOptions) {\n            return {\n                ...super.dropdownOptions,\n                ...this.props.dropdownOptions,\n            };\n        }\n        return super.dropdownOptions;\n    }\n\n    get ulDropdownClass() {\n        let classList = super.ulDropdownClass;\n        if (this.props.dropdownClass) {\n            classList += ` ${this.props.dropdownClass}`;\n        }\n        return classList;\n    }\n\n    get targetDropdown() {\n        return this.props.targetDropdown;\n    }\n\n    _syncInputClick(ev) {\n        ev.stopPropagation();\n        this.onInputClick(ev);\n    }\n\n    async _syncInputValue() {\n        if (this.inputRef.el) {\n            this.inputRef.el.value = this.targetDropdown.value;\n            this.onInput();\n        }\n    }\n\n    /**\n     *\n     * @param indices\n     * @return {boolean}\n     * @private\n     */\n    _isCategory(indices) {\n        const [sourceIndex, optionIndex] = indices;\n        return !!this.sources[sourceIndex]?.options[optionIndex]?.separator;\n    }\n\n    /**\n     * @override\n     */\n    onOptionMouseEnter(indices) {\n        if (!this._isCategory(indices)) {\n            return super.onOptionMouseEnter(...arguments);\n        }\n    }\n\n    /**\n     * @override\n     */\n    onOptionMouseLeave(indices) {\n        if (!this._isCategory(indices)) {\n            return super.onOptionMouseLeave(...arguments);\n        }\n    }\n    isActiveSourceOption(indices) {\n        if (!this._isCategory(indices)) {\n            return super.isActiveSourceOption(...arguments);\n        }\n    }\n    /**\n     * @override\n     */\n    selectOption(indices) {\n        if (!this._isCategory(indices)) {\n            const [sourceIndex, optionIndex] = indices;\n            const { value } = Object.getPrototypeOf(this.sources[sourceIndex].options[optionIndex]);\n            this.targetDropdown.value = value;\n            return super.selectOption(...arguments);\n        }\n    }\n\n    /**\n     * @override\n     */\n    navigate(direction) {\n        super.navigate(direction);\n        if (direction !== 0 && this.state.activeSourceOption) {\n            let [sourceIndex, optionIndex] = this.state.activeSourceOption;\n            const option = this.sources[sourceIndex]?.options[optionIndex];\n            if (option) {\n                if (!!option.separator) {\n                    this.navigate(direction);\n                }\n                const suggestion = Object.getPrototypeOf(option);\n                if (suggestion && suggestion.value) {\n                    this.inputRef.el.value = suggestion.value;\n                }\n            }\n        }\n    }\n\n    /**\n     * @override\n     */\n    onInputFocus(ev) {\n        this.targetDropdown.setSelectionRange(0, this.targetDropdown.value.length);\n        this.props.onFocus(ev);\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { AutoCompleteWithPages } from \"@website/components/autocomplete_with_pages/autocomplete_with_pages\";\n\nexport class UrlAutoComplete extends Component {\n    static props = {\n        options: { type: Object },\n        loadAnchors: { type: Function },\n        targetDropdown: { type: HTMLElement },\n    };\n    static template = \"website.UrlAutoComplete\";\n    static components = { AutoCompleteWithPages };\n\n    _mapItemToSuggestion(item) {\n        return {\n            ...item,\n            classList: item.separator ? \"ui-autocomplete-category\" : \"ui-autocomplete-item\",\n        };\n    }\n\n    get dropdownClass() {\n        const classList = [];\n        for (const key in this.props.options?.classes) {\n            classList.push(key, this.props.options.classes[key]);\n        }\n        return classList.join(\" \")\n    }\n\n    get dropdownOptions() {\n        const options = {};\n        if (this.props.options?.position) {\n            options.position = this.props.options?.position;\n        }\n        return options;\n    }\n\n    get sources() {\n        return [\n            {\n                optionTemplate: \"website.AutoCompleteWithPagesItem\",\n                options: async (term) => {\n                    if (term[0] === \"#\") {\n                        const anchors = await this.props.loadAnchors(\n                            term,\n                            this.props.options && this.props.options.body\n                        );\n                        return anchors.map((anchor) =>\n                            this._mapItemToSuggestion({ label: anchor, value: anchor })\n                        );\n                    } else if (term.startsWith(\"http\") || term.length === 0) {\n                        // avoid useless call to /website/get_suggested_links\n                        return [];\n                    }\n                    if (this.props.options.isDestroyed?.()) {\n                        return [];\n                    }\n                    const res = await rpc(\"/website/get_suggested_links\", {\n                        needle: term,\n                        limit: 15,\n                    });\n                    let choices = res.matching_pages;\n                    res.others.forEach((other) => {\n                        if (other.values.length) {\n                            choices = choices.concat(\n                                [{ separator: other.title, label: other.title }],\n                                other.values\n                            );\n                        }\n                    });\n                    return choices.map(this._mapItemToSuggestion);\n                },\n            },\n        ];\n    }\n\n    onSelect(selectedSubjection, { input }) {\n        const { value } = Object.getPrototypeOf(selectedSubjection);\n        input.value = value;\n        this.props.targetDropdown.value = value;\n        this.props.options.urlChosen?.();\n    }\n\n    onInput({ inputValue }) {\n        this.props.targetDropdown.value = inputValue;\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nimport { markup } from \"@odoo/owl\";\nimport { omit } from \"@web/core/utils/objects\";\n\nexport function addMedia(position = \"right\") {\n    return {\n        trigger: `.modal-content footer .btn-primary`,\n        content: markup(_t(\"<b>Add</b> the selected image.\")),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\nexport function assertCssVariable(variableName, variableValue, trigger = ':iframe body') {\n    return {\n        isActive: [\"auto\"],\n        content: `Check CSS variable ${variableName}=${variableValue}`,\n        trigger: trigger,\n        run() {\n            const styleValue = getComputedStyle(this.anchor).getPropertyValue(variableName);\n            if ((styleValue && styleValue.trim().replace(/[\"']/g, '')) !== variableValue.trim().replace(/[\"']/g, '')) {\n                throw new Error(`Failed precondition: ${variableName}=${styleValue} (should be ${variableValue})`);\n            }\n        },\n    };\n}\nexport function assertPathName(pathName, trigger) {\n    return {\n        content: `Check if we have been redirected to ${pathName}`,\n        trigger: trigger,\n        run: () => {\n            if (!window.location.pathname.startsWith(pathName)) {\n                console.error(`We should be on ${pathName}.`);\n            }\n        }\n    };\n}\n\nexport function changeBackground(snippet, position = \"bottom\") {\n    return [\n        {\n            trigger: \".o_we_customize_panel .o_we_bg_success\",\n        content: markup(_t(\"<b>Customize</b> any block through this menu. Try to change the background image of this block.\")),\n            tooltipPosition: position,\n            run: \"click\",\n        },\n    ];\n}\n\nexport function changeBackgroundColor(position = \"bottom\") {\n    return {\n        trigger: \".o_we_customize_panel .o_we_color_preview\",\n        content: markup(_t(\"<b>Customize</b> any block through this menu. Try to change the background color of this block.\")),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\nexport function selectColorPalette(position = \"left\") {\n    return {\n        trigger:\n            \".o_we_customize_panel .o_we_so_color_palette we-selection-items, .o_we_customize_panel .o_we_color_preview\",\n        content: markup(_t(`<b>Select</b> a Color Palette.`)),\n        tooltipPosition: position,\n        run: 'click',\n    };\n}\n\nexport function changeColumnSize(position = \"right\") {\n    return {\n        trigger: `:iframe .oe_overlay.o_draggable.o_we_overlay_sticky.oe_active .o_handle.e`,\n        content: markup(_t(\"<b>Slide</b> this button to change the column size.\")),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\nexport function changeImage(snippet, position = \"bottom\") {\n    return [\n        {\n            trigger: \"body.editor_enable\",\n        },\n        {\n            trigger: snippet.id ? `#wrapwrap .${snippet.id} img` : snippet,\n        content: markup(_t(\"<b>Double click on an image</b> to change it with one of your choice.\")),\n            tooltipPosition: position,\n            run: \"dblclick\",\n        },\n    ];\n}\n\n/**\n    wTourUtils.changeOption('HeaderTemplate', '[data-name=\"header_alignment_opt\"]', _t('alignment')),\n    By default, prevents the step from being active if a palette is opened.\n    Set allowPalette to true to select options within a palette.\n*/\nexport function changeOption(optionName, weName = '', optionTooltipLabel = '', position = \"bottom\", allowPalette = false) {\n    const noPalette = allowPalette ? '' : '.o_we_customize_panel:not(:has(.o_we_so_color_palette.o_we_widget_opened))';\n    const option_block = `${noPalette} we-customizeblock-option[class='snippet-option-${optionName}']`;\n    return {\n        trigger: `${option_block} ${weName}, ${option_block} [title='${weName}']`,\n        content: markup(_t(\"<b>Click</b> on this option to change the %s of the block.\", optionTooltipLabel)),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\nexport function selectNested(trigger, optionName, altTrigger = null, optionTooltipLabel = '', position = \"top\", allowPalette = false) {\n    const noPalette = allowPalette ? '' : '.o_we_customize_panel:not(:has(.o_we_so_color_palette.o_we_widget_opened))';\n    const option_block = `${noPalette} we-customizeblock-option[class='snippet-option-${optionName}']`;\n    return {\n        trigger: trigger + (altTrigger ? `, ${option_block} ${altTrigger}` : \"\"),\n        content: markup(_t(\"<b>Select</b> a %s.\", optionTooltipLabel)),\n        tooltipPosition: position,\n        run: 'click',\n    };\n}\n\nexport function changePaddingSize(direction) {\n    let paddingDirection = \"n\";\n    let position = \"top\";\n    if (direction === \"bottom\") {\n        paddingDirection = \"s\";\n        position = \"bottom\";\n    }\n    return {\n        trigger: `:iframe .oe_overlay.o_draggable.o_we_overlay_sticky.oe_active .o_handle.${paddingDirection}`,\n        content: markup(_t(\"<b>Slide</b> this button to change the %s padding\", direction)),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\n/**\n * Checks if an element is visible on the screen, i.e., not masked by another\n * element.\n *\n * @param {String} elementSelector The selector of the element to be checked.\n * @returns {Object} The steps required to check if the element is visible.\n */\nexport function checkIfVisibleOnScreen(elementSelector) {\n    return {\n        content: \"Check if the element is visible on screen\",\n        trigger: `${elementSelector}`,\n        run() {\n            const boundingRect = this.anchor.getBoundingClientRect();\n            const centerX = boundingRect.left + boundingRect.width / 2;\n            const centerY = boundingRect.top + boundingRect.height / 2;\n            const iframeDocument = document.querySelector(\".o_iframe\").contentDocument;\n            const el = iframeDocument.elementFromPoint(centerX, centerY);\n            if (!this.anchor.contains(el)) {\n                console.error(\"The element is not visible on screen\");\n            }\n        },\n    };\n}\n\n/**\n * Simple click on an element in the page.\n * @param {*} elementName\n * @param {*} selector\n */\nexport function clickOnElement(elementName, selector) {\n    return {\n        content: `Clicking on the ${elementName}`,\n        trigger: selector,\n        run: 'click'\n    };\n}\n\n/**\n * Click on the top right edit button and wait for the edit mode\n *\n * @param {string} position Where the purple arrow will show up\n */\nexport function clickOnEditAndWaitEditMode(position = \"bottom\") {\n    return [{\n        content: markup(_t(\"<b>Click Edit</b> to start designing your homepage.\")),\n        trigger: \".o_menu_systray .o_edit_website_container a\",\n        tooltipPosition: position,\n        run: \"click\",\n    }, {\n        isActive: [\"auto\"], // Checking step only for automated tests\n        content: \"Check that we are in edit mode\",\n        trigger: \".o_website_preview.editor_enable.editor_has_snippets\",\n    }];\n}\n\n/**\n * Click on the top right edit dropdown, then click on the edit dropdown item\n * and wait for the edit mode\n *\n * @param {string} position Where the purple arrow will show up\n */\nexport function clickOnEditAndWaitEditModeInTranslatedPage(position = \"bottom\") {\n    return [{\n        content: markup(_t(\"<b>Click Edit</b> dropdown\")),\n        trigger: \".o_edit_website_container button\",\n        tooltipPosition: position,\n        run: \"click\",\n    }, {\n        content: markup(_t(\"<b>Click Edit</b> to start designing your homepage.\")),\n        trigger: \".o_edit_website_dropdown_item\",\n        tooltipPosition: position,\n        run: \"click\",\n    }, {\n        isActive: [\"auto\"], // Checking step only for automated tests\n        content: \"Check that we are in edit mode\",\n        trigger: \".o_website_preview.editor_enable.editor_has_snippets\",\n    }];\n}\n\n/**\n * Simple click on a snippet in the edition area\n * @param {*} snippet\n * @param {*} position\n */\nexport function clickOnSnippet(snippet, position = \"bottom\") {\n    const trigger = snippet.id ? `#wrapwrap .${snippet.id}` : snippet;\n    return [\n        {\n            trigger: \"body.editor_has_snippets\",\n            noPrepend: true,\n        },\n        {\n            trigger: `:iframe ${trigger}`,\n        content: markup(_t(\"<b>Click on a snippet</b> to access its options menu.\")),\n            tooltipPosition: position,\n            run: \"click\",\n        },\n    ];\n}\n\nexport function clickOnSave(position = \"bottom\", timeout) {\n    return [\n        {\n            trigger: \"#oe_snippets:not(:has(.o_we_ongoing_insertion))\",\n        },\n        {\n            trigger: \"body:not(:has(.o_dialog))\",\n            noPrepend: true,\n        },\n        {\n            trigger:\n                'div:not(.o_loading_dummy) > #oe_snippets button[data-action=\"save\"]:not([disabled])',\n            // TODO this should not be needed but for now it better simulates what\n            // an human does. By the time this was added, it's technically possible\n            // to drag and drop a snippet then immediately click on save and have\n            // some problem. Worst case probably is a traceback during the redirect\n            // after save though so it's not that big of an issue. The problem will\n            // of course be solved (or at least prevented in stable). More details\n            // in related commit message.\n        content: markup(_t(\"Good job! It's time to <b>Save</b> your work.\")),\n            tooltipPosition: position,\n            timeout: timeout,\n            run: \"click\",\n        },\n        {\n            isActive: [\"auto\"], // Just making sure save is finished in automatic tests\n            trigger: \":iframe body:not(.editor_enable)\",\n            noPrepend: true,\n            timeout: timeout,\n        },\n    ];\n}\n\n/**\n * Click on a snippet's text to modify its content\n * @param {*} snippet\n * @param {*} element Target the element which should be rewrite\n * @param {*} position\n */\nexport function clickOnText(snippet, element, position = \"bottom\") {\n    return [\n        {\n            trigger: \":iframe body.editor_enable\",\n        },\n        {\n            trigger: snippet.id ? `:iframe #wrapwrap .${snippet.id} ${element}` : snippet,\n        content: markup(_t(\"<b>Click on a text</b> to start editing it.\")),\n            tooltipPosition: position,\n            run: \"click\",\n        },\n    ];\n}\n\n/**\n * Selects a category or an inner snippet from the snippets menu and insert it\n * in the page.\n * @param {*} snippet contain the id and the name of the targeted snippet. If it\n * contains a group it means that the snippet is shown in the \"add snippets\"\n * dialog.\n * @param {*} position Where the purple arrow will show up\n */\nexport function insertSnippet(snippet, position = \"bottom\") {\n    const blockEl = snippet.groupName || snippet.name;\n    const insertSnippetSteps = [{\n        trigger: \".o_website_preview.editor_enable.editor_has_snippets\",\n        noPrepend: true,\n    }];\n    if (snippet.groupName) {\n        insertSnippetSteps.push({\n            content: markup(_t(\"Click on the <b>%s</b> category.\", blockEl)),\n            trigger: `#oe_snippets .oe_snippet[name=\"${blockEl}\"].o_we_draggable .oe_snippet_thumbnail:not(.o_we_ongoing_insertion)`,\n            tooltipPosition: position,\n            run: \"click\",\n        },\n        {\n            content: markup(_t(\"Click on the <b>%s</b> building block.\", snippet.name)),\n            // FIXME `:not(.d-none)` should obviously not be needed but it seems\n            // currently needed when using a tour in user/interactive mode.\n            trigger: `:iframe .o_snippet_preview_wrap[data-snippet-id=\"${snippet.id}\"]:not(.d-none)`,\n            noPrepend: true,\n            tooltipPosition: \"top\",\n            run: \"click\",\n        },\n        {\n            trigger: `#oe_snippets .oe_snippet[name=\"${blockEl}\"].o_we_draggable .oe_snippet_thumbnail:not(.o_we_ongoing_insertion)`,\n        });\n    } else {\n        insertSnippetSteps.push({\n            content: markup(_t(\"Drag the <b>%s</b> block and drop it at the bottom of the page.\", blockEl)),\n            trigger: `#oe_snippets .oe_snippet[name=\"${blockEl}\"].o_we_draggable .oe_snippet_thumbnail:not(.o_we_ongoing_insertion)`,\n            tooltipPosition: position,\n            run: \"drag_and_drop :iframe #wrapwrap > footer\",\n        });\n    }\n    return insertSnippetSteps;\n}\n\nexport function goBackToBlocks(position = \"bottom\") {\n    return {\n        trigger: '.o_we_add_snippet_btn',\n        content: _t(\"Click here to go back to block tab.\"),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\nexport function goToTheme(position = \"bottom\") {\n    return [\n        {\n            trigger: \"#oe_snippets.o_loaded\",\n        },\n        {\n            trigger: \".o_we_customize_theme_btn\",\n            content: _t(\"Go to the Theme tab\"),\n            tooltipPosition: position,\n            run: \"click\",\n        },\n    ];\n}\n\nexport function selectHeader(position = \"bottom\") {\n    return {\n        trigger: `:iframe header#top`,\n        content: markup(_t(`<b>Click</b> on this header to configure it.`)),\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\nexport function selectSnippetColumn(snippet, index = 0, position = \"bottom\") {\n     return {\n        trigger: `:iframe #wrapwrap .${snippet.id} .row div[class*=\"col-lg-\"]:eq(${index})`,\n        content: markup(_t(\"<b>Click</b> on this column to access its options.\")),\n         tooltipPosition: position,\n        run: \"click\",\n     };\n}\n\nexport function prepend_trigger(steps, prepend_text='') {\n    for (const step of steps) {\n        if (!step.noPrepend && prepend_text) {\n            step.trigger = prepend_text + step.trigger;\n        }\n    }\n    return steps;\n}\n\nexport function getClientActionUrl(path, edition) {\n    let url = `/odoo/action-website.website_preview`;\n    if (path) {\n        url += `?path=${encodeURIComponent(path)}`;\n    }\n    if (edition) {\n        url += `${path ? '&' : '?'}enable_editor=1`;\n    }\n    return url;\n}\n\nexport function clickOnExtraMenuItem(stepOptions, backend = false) {\n    return Object.assign({\n        content: \"Click on the extra menu dropdown toggle if it is there\",\n        trigger: `${backend ? \":iframe\" : \"\"} .top_menu`,\n        async run(actions) {\n            const extraMenuButton = this.anchor.querySelector(\".o_extra_menu_items a.nav-link\");\n            // Don't click on the extra menu button if it's already visible.\n            if (extraMenuButton && !extraMenuButton.classList.contains(\"show\")) {\n                await actions.click(extraMenuButton);\n            }\n        },\n    }, stepOptions);\n}\n\n/**\n * Registers a tour that will go in the website client action.\n *\n * @param {string} name The tour's name\n * @param {object} options The tour options\n * @param {string} options.url The page to edit\n * @param {boolean} [options.edition] If the tour starts in edit mode\n * @param {() => TourStep[]} steps The steps of the tour. Has to be a function to avoid direct interpolation of steps.\n */\nexport function registerWebsitePreviewTour(name, options, steps) {\n    if (typeof steps !== \"function\") {\n        throw new Error(`tour.steps has to be a function that returns TourStep[]`);\n    }\n    return registry.category(\"web_tour.tours\").add(name, {\n        ...omit(options, \"edition\"),\n        url: getClientActionUrl(options.url, !!options.edition),\n        steps: () => {\n            const tourSteps = [...steps()];\n            // Note: for both non edit mode and edit mode, we set a high timeout for the\n            // first step. Indeed loading both the backend and the frontend (in the\n            // iframe) and potentially starting the edit mode can take a long time in\n            // automatic tests. We'll try and decrease the need for this high timeout\n            // of course.\n            if (options.edition) {\n                tourSteps.unshift({\n                    isActive: [\"auto\"],\n                    content: \"Wait for the edit mode to be started\",\n                    trigger: \".o_website_preview.editor_enable.editor_has_snippets\",\n                    timeout: 30000,\n                });\n            } else {\n                tourSteps[0].timeout = 20000;\n            }\n            return tourSteps.map((step) => {\n                delete step.noPrepend;\n                return step;\n            });\n        },\n    });\n}\n\nexport function registerThemeHomepageTour(name, steps) {\n    if (typeof steps !== \"function\") {\n        throw new Error(`tour.steps has to be a function that returns TourStep[]`);\n    }\n    return registerWebsitePreviewTour(name, {\n        url: '/',\n        saveAs: \"homepage\", // disable manual mode for theme homepage tours - FIXME\n        },\n        () => [\n            ...clickOnEditAndWaitEditMode(),\n            ...prepend_trigger(\n                steps().concat(clickOnSave()),\n                \".o_website_preview[data-view-xmlid='website.homepage'] \"\n            ),\n    ]);\n}\n\nexport function registerBackendAndFrontendTour(name, options, steps) {\n    if (typeof steps !== \"function\") {\n        throw new Error(`tour.steps has to be a function that returns TourStep[]`);\n    }\n    if (window.location.pathname === '/odoo') {\n        return registerWebsitePreviewTour(name, options, () => {\n            const newSteps = [];\n            for (const step of steps()) {\n                const newStep = Object.assign({}, step);\n                newStep.trigger = `:iframe ${step.trigger}`;\n                newSteps.push(newStep);\n            }\n            return newSteps;\n        });\n    }\n\n    return registry.category(\"web_tour.tours\").add(name, {\n        url: options.url,\n        steps: () => {\n            return steps();\n        },\n    });\n}\n\n/**\n * Selects an element inside a we-select, if the we-select is from a m2o widget, searches for it.\n *\n * @param widgetName {string} The widget's data-name\n * @param elementName {string} the element to search\n * @param searchNeeded {Boolean} if the widget is a m2o widget and a search is needed\n */\nexport function selectElementInWeSelectWidget(widgetName, elementName, searchNeeded = false) {\n    const steps = [clickOnElement(`${widgetName} toggler`, `we-select[data-name=${widgetName}] we-toggler`)];\n\n    if (searchNeeded) {\n        steps.push({\n            content: `Inputing ${elementName} in m2o widget search`,\n            trigger: `we-select[data-name=${widgetName}] div.o_we_m2o_search input`,\n            run: `edit ${elementName}`,\n        });\n    }\n    steps.push(clickOnElement(`${elementName} in the ${widgetName} widget`,\n        `we-select[data-name=\"${widgetName}\"] we-button:contains(\"${elementName}\"), ` +\n        `we-select[data-name=\"${widgetName}\"] we-button[data-select-label=\"${elementName}\"]`));\n    return steps;\n}\n\n/**\n * Switches to a different website by clicking on the website switcher.\n *\n * @param {number} websiteId - The ID of the website to switch to.\n * @param {string} websiteName - The name of the website to switch to.\n * @returns {Array} - The steps required to perform the website switch.\n */\nexport function switchWebsite(websiteId, websiteName) {\n    return [{\n        content: `Click on the website switch to switch to website '${websiteName}'`,\n        trigger: '.o_website_switcher_container button',\n        run: \"click\",\n    },\n    {\n        trigger: `:iframe html:not([data-website-id=\"${websiteId}\"])`,\n    },\n    {\n        content: `Switch to website '${websiteName}'`,\n        trigger: `.o-dropdown--menu .dropdown-item:contains(\"${websiteName}\")`,\n        run: \"click\",\n    }, {\n        content: \"Wait for the iframe to be loaded\",\n        // The page reload generates assets for the new website, it may take\n        // some time\n        timeout: 20000,\n        trigger: `:iframe html[data-website-id=\"${websiteId}\"]`,\n    }];\n}\n\n/**\n * Toggles the mobile preview on or off.\n *\n * @param {Boolean} toggleOn true to toggle the mobile preview on, false to\n *     toggle it off.\n * @returns {Array}\n */\nexport function toggleMobilePreview(toggleOn) {\n    const onOrOff = toggleOn ? \"on\" : \"off\";\n    const mobileOnSelector = \".o_is_mobile\";\n    const mobileOffSelector = \":not(.o_is_mobile)\";\n    return [\n        {\n            trigger: `:iframe html${toggleOn ? mobileOffSelector : mobileOnSelector}`,\n        },\n        {\n            content: `Toggle the mobile preview ${onOrOff}`,\n            trigger: \".o_we_website_top_actions [data-action='mobile']\",\n            run: \"click\",\n        },\n        {\n            content: `Check that the mobile preview is ${onOrOff}`,\n            trigger: `:iframe html${toggleOn ? mobileOnSelector : mobileOffSelector}`,\n        },\n    ];\n}\n", "/** @odoo-module */\n\nimport { loadJS } from \"@web/core/assets\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport publicRootData from '@web/legacy/js/public/public_root';\nimport \"@website/libs/zoomodoo/zoomodoo\";\nimport { pick } from \"@web/core/utils/objects\";\n\nimport { markup } from \"@odoo/owl\";\n\nexport const WebsiteRoot = publicRootData.PublicRoot.extend({\n    events: Object.assign({}, publicRootData.PublicRoot.prototype.events || {}, {\n        'click .js_change_lang': '_onLangChangeClick',\n        'click .js_publish_management .js_publish_btn': '_onPublishBtnClick',\n        'shown.bs.modal': '_onModalShown',\n    }),\n    custom_events: Object.assign({}, publicRootData.PublicRoot.prototype.custom_events || {}, {\n        'gmap_api_request': '_onGMapAPIRequest',\n        'gmap_api_key_request': '_onGMapAPIKeyRequest',\n        'ready_to_clean_for_save': '_onWidgetsStopRequest',\n        'seo_object_request': '_onSeoObjectRequest',\n        'will_remove_snippet': '_onWidgetsStopRequest',\n    }),\n\n    /**\n     * @override\n     */\n    init() {\n        this.isFullscreen = false;\n        this.notification = this.bindService(\"notification\");\n        this.orm = this.bindService(\"orm\");\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        // Enable magnify on zommable img\n        this.$('.zoomable img[data-zoom]').zoomOdoo();\n\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _getContext: function (context) {\n        var html = document.documentElement;\n        return Object.assign({\n            'website_id': html.getAttribute('data-website-id') | 0,\n        }, this._super.apply(this, arguments));\n    },\n    /**\n     * @override\n     */\n    _getExtraContext: function (context) {\n        var html = document.documentElement;\n        return Object.assign({\n            'editable': !!(html.dataset.editable || $('[data-oe-model]').length), // temporary hack, this should be done in python\n            'translatable': !!html.dataset.translatable,\n            'edit_translations': !!html.dataset.edit_translations,\n        }, this._super.apply(this, arguments));\n    },\n    /**\n     * @private\n     * @param {boolean} [refetch=false]\n     */\n    async _getGMapAPIKey(refetch) {\n        if (refetch || !this._gmapAPIKeyProm) {\n            this._gmapAPIKeyProm = 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 this._gmapAPIKeyProm;\n    },\n    /**\n     * @override\n     */\n    _getPublicWidgetsRegistry: function (options) {\n        var registry = this._super.apply(this, arguments);\n        if (options.editableMode) {\n            const toPick = Object.keys(registry).filter((key) => {\n                const PublicWidget = registry[key];\n                return !PublicWidget.prototype.disabledInEditableMode;\n            });\n            return pick(registry, ...toPick);\n        }\n        return registry;\n    },\n    /**\n     * @private\n     * @param {boolean} [editableMode=false]\n     * @param {boolean} [refetch=false]\n     */\n    async _loadGMapAPI(editableMode, refetch) {\n        // Note: only need refetch to reload a configured key and load the\n        // library. If the library was loaded with a correct key and that the\n        // key changes meanwhile... it will not work but we can agree the user\n        // can bother to reload the page at that moment.\n        if (refetch || !this._gmapAPILoading) {\n            this._gmapAPILoading = new Promise(async resolve => {\n                const key = await this._getGMapAPIKey(refetch);\n\n                window.odoo_gmap_api_post_load = (async function odoo_gmap_api_post_load() {\n                    await this._startWidgets($(\"section.s_google_map\"), {editableMode: editableMode});\n                    resolve(key);\n                }).bind(this);\n\n                if (!key) {\n                    if (!editableMode && user.isAdmin) {\n                        const message = _t(\"Cannot load google map.\");\n                        const urlTitle = _t(\"Check your configuration.\");\n                        this.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                    this._gmapAPILoading = false;\n                    return;\n                }\n                await loadJS(`https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=places&callback=odoo_gmap_api_post_load&key=${encodeURIComponent(key)}`);\n            });\n        }\n        return this._gmapAPILoading;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _onWidgetsStartRequest: function (ev) {\n        ev.data.options = Object.assign({}, ev.data.options || {});\n        ev.data.options.editableMode = ev.data.editableMode;\n        this._super.apply(this, arguments);\n    },\n    /**\n     * @todo review\n     * @private\n     */\n    _onLangChangeClick: function (ev) {\n        ev.preventDefault();\n        // In edit mode, the client action redirects the iframe to the correct\n        // location with the chosen language.\n        if (document.body.classList.contains('editor_enable')) {\n            return;\n        }\n        var $target = $(ev.currentTarget);\n        // retrieve the hash before the redirect\n        var redirect = {\n            lang: encodeURIComponent($target.data('url_code')),\n            url: encodeURIComponent($target.attr('href').replace(/[&?]edit_translations[^&?]+/, '')),\n            hash: encodeURIComponent(window.location.hash)\n        };\n        window.location.href = `/website/lang/${redirect.lang}?r=${redirect.url}${redirect.hash}`;\n    },\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    async _onGMapAPIRequest(ev) {\n        ev.stopPropagation();\n        const apiKey = await this._loadGMapAPI(ev.data.editableMode, ev.data.refetch);\n        ev.data.onSuccess(apiKey);\n    },\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    async _onGMapAPIKeyRequest(ev) {\n        ev.stopPropagation();\n        const apiKey = await this._getGMapAPIKey(ev.data.refetch);\n        ev.data.onSuccess(apiKey);\n    },\n    /**\n    /**\n     * Checks information about the page SEO object.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onSeoObjectRequest: function (ev) {\n        var res = this._unslugHtmlDataObject('seo-object');\n        ev.data.callback(res);\n    },\n    /**\n     * Returns a model/id object constructed from html data attribute.\n     *\n     * @private\n     * @param {string} dataAttr\n     * @returns {Object} an object with 2 keys: model and id, or null\n     * if not found\n     */\n    _unslugHtmlDataObject: function (dataAttr) {\n        var repr = $('html').data(dataAttr);\n        var match = repr && repr.match(/(.+)\\((\\d+),(.*)\\)/);\n        if (!match) {\n            return null;\n        }\n        return {\n            model: match[1],\n            id: match[2] | 0,\n        };\n    },\n    /**\n     * @todo review\n     * @private\n     */\n    _onPublishBtnClick: function (ev) {\n        ev.preventDefault();\n        if (document.body.classList.contains('editor_enable')) {\n            return;\n        }\n\n        const publishEl = ev.currentTarget.closest(\".js_publish_management\");\n        this.orm.call(\n            publishEl.dataset.object,\n            \"website_publish_button\",\n            [[parseInt(publishEl.dataset.id, 10)]]\n        ).then(function (result) {\n            publishEl.classList.toggle(\"css_published\", result);\n            publishEl.classList.toggle(\"css_unpublished\", !result);\n            const itemEl = publishEl.closest(\"[data-publish]\");\n            if (itemEl) {\n                itemEl.dataset.publish = result ? 'on' : 'off';\n            }\n        });\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onModalShown: function (ev) {\n        $(ev.target).addClass('modal_shown');\n    },\n});\n\nexport default {\n    WebsiteRoot: WebsiteRoot,\n};\n", "/** @odoo-module **/\n\n/**\n * Tweaks the website rendering so that the old browsers correctly render the\n * content too.\n */\n\n// Check if flex is supported and add the info as an attribute of the HTML\n// element so that css selectors can match it (only if not supported)\nvar htmlStyle = document.documentElement.style;\nvar isFlexSupported = (('flexWrap' in htmlStyle)\n                    || ('WebkitFlexWrap' in htmlStyle)\n                    || ('msFlexWrap' in htmlStyle));\nif (!isFlexSupported) {\n    document.documentElement.setAttribute('data-no-flex', '');\n}\n\nexport default {\n    isFlexSupported: isFlexSupported,\n};\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport animations from \"@website/js/content/snippets.animation\";\nexport const extraMenuUpdateCallbacks = [];\nimport { SIZES, utils as uiUtils } from \"@web/core/ui/ui_service\";\nimport { compensateScrollbar } from \"@web/core/utils/scrolling\";\n\n// The header height may vary with sections hidden on scroll (see the class\n// `o_header_hide_on_scroll`). To avoid scroll jumps, we cache the value.\nlet headerHeight;\n\nconst BaseAnimatedHeader = animations.Animation.extend({\n    disabledInEditableMode: false,\n    effects: [{\n        startEvents: 'scroll',\n        update: '_updateHeaderOnScroll',\n    }, {\n        startEvents: 'resize',\n        update: '_updateHeaderOnResize',\n    }],\n\n    /**\n     * @constructor\n     */\n    init: function () {\n        this._super(...arguments);\n        this.fixedHeader = false;\n        this.scrolledPoint = 0;\n        this.hasScrolled = false;\n        this.closeOpenedMenus = false;\n        this.scrollHeightTooShort = false;\n        this.scrollableEl = $().getScrollingElement()[0];\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        this.$main = this.$el.next('main');\n        this.isOverlayHeader = !!this.$el.closest('.o_header_overlay, .o_header_overlay_theme').length;\n        this.hiddenOnScrollEl = this.el.querySelector(\".o_header_hide_on_scroll\");\n\n        // While scrolling through navbar menus on medium devices, body should\n        // not be scrolled with it.\n        const disableScroll = function () {\n            if (uiUtils.getSize() < SIZES.LG) {\n                $(document.body).addClass('overflow-hidden');\n            }\n        };\n        const enableScroll = function () {\n            $(document.body).removeClass('overflow-hidden');\n        };\n        this.$navbarOffcanvases = this.$el.find(\".offcanvas\");\n        this.$navbarOffcanvases\n            .on(\"show.bs.offcanvas.BaseAnimatedHeader\", disableScroll)\n            .on(\"hide.bs.offcanvas.BaseAnimatedHeader\", enableScroll);\n\n        // Compatibility: can probably be removed, there is no such elements in\n        // default navbars... although it could be used by custo.\n        this.$navbarCollapses = this.$el.find('.navbar-collapse');\n        this.$navbarCollapses\n            .on(\"show.bs.collapse.BaseAnimatedHeader\", disableScroll)\n            .on(\"hide.bs.collapse.BaseAnimatedHeader\", enableScroll);\n\n        // We can rely on transitionend which is well supported but not on\n        // transitionstart, so we listen to a custom odoo event.\n        this._transitionCount = 0;\n        this.$el.on('odoo-transitionstart.BaseAnimatedHeader', () => {\n            this.el.classList.add('o_transitioning');\n            this._adaptToHeaderChangeLoop(1);\n        });\n        this.$el.on('transitionend.BaseAnimatedHeader', () => this._adaptToHeaderChangeLoop(-1));\n\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        this._toggleFixedHeader(false);\n        this.$el.removeClass('o_header_affixed o_header_is_scrolled o_header_no_transition o_transitioning');\n        this.$navbarOffcanvases.off(\".BaseAnimatedHeader\");\n        this.$navbarCollapses.off('.BaseAnimatedHeader');\n        this.$el.off('.BaseAnimatedHeader');\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Adapt the 'right' css property of the header by adding the size of a\n     * scrollbar if any.\n     *\n     * @private\n     */\n    _adaptFixedHeaderPosition() {\n        compensateScrollbar(this.el, this.fixedHeader, false, 'right');\n    },\n    /**\n     * @private\n     */\n    _adaptToHeaderChange: function () {\n        this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerUnactive();\n        this._updateMainPaddingTop();\n        // Take menu into account when `scrollTo()` is used whenever it is\n        // visible - be it floating, fully displayed or partially hidden.\n        this.el.classList.toggle('o_top_fixed_element', this._isShown());\n\n        for (const callback of extraMenuUpdateCallbacks) {\n            callback();\n        }\n        this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerActive();\n    },\n    /**\n     * @private\n     * @param {integer} [addCount=0]\n     */\n    _adaptToHeaderChangeLoop: function (addCount = 0) {\n        this._adaptToHeaderChange();\n\n        this._transitionCount += addCount;\n        this._transitionCount = Math.max(0, this._transitionCount);\n\n        // As long as we detected a transition start without its related\n        // transition end, keep updating the main padding top.\n        if (this._transitionCount > 0) {\n            window.requestAnimationFrame(() => this._adaptToHeaderChangeLoop());\n\n            // The normal case would be to have the transitionend event to be\n            // fired but we cannot rely on it, so we use a timeout as fallback.\n            if (addCount !== 0) {\n                clearTimeout(this._changeLoopTimer);\n                this._changeLoopTimer = setTimeout(() => {\n                    this._adaptToHeaderChangeLoop(-this._transitionCount);\n                }, 500);\n            }\n        } else {\n            // When we detected all transitionend events, we need to stop the\n            // setTimeout fallback.\n            clearTimeout(this._changeLoopTimer);\n            this.el.classList.remove('o_transitioning');\n        }\n    },\n    /**\n     * Scrolls to correctly display the section specified in the URL\n     *\n     * @private\n     */\n    _adjustUrlAutoScroll() {\n        // When the url contains #aRandomSection, prevent the navbar to overlap\n        // on the section, for this, we scroll as many px as the navbar height.\n        if (!this.editableMode) {\n            this.scrollableEl.scrollBy(0, -this.el.offsetHeight);\n        }\n    },\n    /**\n     * @private\n     */\n    _computeTopGap() {\n        return 0;\n    },\n    /**\n     * @private\n     */\n    _isShown() {\n        return true;\n    },\n    /**\n     * @private\n     * @param {boolean} [useFixed=true]\n     */\n    _toggleFixedHeader: function (useFixed = true) {\n        this.fixedHeader = useFixed;\n        this._adaptToHeaderChange();\n        this.el.classList.toggle('o_header_affixed', useFixed);\n        this._adaptFixedHeaderPosition();\n    },\n    /**\n     * @private\n     */\n    _updateMainPaddingTop: function () {\n        headerHeight ||= this.el.getBoundingClientRect().height;\n        this.topGap = this._computeTopGap();\n\n        if (this.isOverlayHeader) {\n            return;\n        }\n        this.$main.css('padding-top', this.fixedHeader ? headerHeight : '');\n    },\n    /**\n     * Checks if the size of the header will decrease by adding the\n     * 'o_header_is_scrolled' class. If so, we do not add this class if the\n     * remaining scroll height is not enough to stay above 'this.scrolledPoint'\n     * after the transition, otherwise it causes the scroll position to move up\n     * again below 'this.scrolledPoint' and trigger an infinite loop.\n     *\n     * @todo header effects should be improved in the future to not ever change\n     * the page scroll-height during their animation. The code would probably be\n     * simpler but also prevent having weird scroll \"jumps\" during animations\n     * (= depending on the logo height after/before scroll, a scroll step (one\n     * mousewheel event for example) can be bigger than other ones).\n     *\n     * @private\n     * @returns {boolean}\n     */\n    _scrollHeightTooShort() {\n        const scrollEl = this.scrollableEl;\n        const remainingScroll = (scrollEl.scrollHeight - scrollEl.clientHeight) - this.scrolledPoint;\n        const clonedHeader = this.el.cloneNode(true);\n        scrollEl.append(clonedHeader);\n        clonedHeader.classList.add('o_header_is_scrolled', 'o_header_affixed', 'o_header_no_transition');\n        const endHeaderHeight = clonedHeader.offsetHeight;\n        clonedHeader.remove();\n        const heightDiff = headerHeight - endHeaderHeight;\n        return heightDiff > 0 ? remainingScroll <= heightDiff : false;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when the window is scrolled\n     *\n     * @private\n     * @param {integer} scroll\n     */\n    _updateHeaderOnScroll: function (scroll) {\n        // Disable css transition if refresh with scrollTop > 0\n        if (!this.hasScrolled) {\n            this.hasScrolled = true;\n            if (scroll > 0) {\n                this.$el.addClass('o_header_no_transition');\n                this._adjustUrlAutoScroll();\n            }\n        } else {\n            this.$el.removeClass('o_header_no_transition');\n            this.closeOpenedMenus = true;\n        }\n\n        // Indicates the page is scrolled, the logo size is changed.\n        const headerIsScrolled = (scroll > this.scrolledPoint);\n        if (this.headerIsScrolled !== headerIsScrolled) {\n            this.scrollHeightTooShort = headerIsScrolled && this._scrollHeightTooShort();\n            if (!this.scrollHeightTooShort) {\n                this.el.classList.toggle('o_header_is_scrolled', headerIsScrolled);\n                this.$el.trigger('odoo-transitionstart');\n                this.headerIsScrolled = headerIsScrolled;\n            }\n        }\n\n        if (this.closeOpenedMenus) {\n            // Hide only the open dropdowns.\n            this.el.querySelectorAll(\".dropdown-toggle.show\").forEach(dropdownToggleEl => {\n                Dropdown.getOrCreateInstance(dropdownToggleEl).hide();\n            });\n        }\n    },\n    /**\n     * Called when the window is resized\n     *\n     * @private\n     */\n    _updateHeaderOnResize: function () {\n        this._adaptFixedHeaderPosition();\n        if (document.body.classList.contains('overflow-hidden')\n                && uiUtils.getSize() >= SIZES.LG) {\n            this.el.querySelectorAll(\".offcanvas.show\").forEach(offcanvasEl => {\n                Offcanvas.getOrCreateInstance(offcanvasEl).hide();\n            });\n            // Compatibility: can probably be removed, there is no such elements\n            // in default navbars... although it could be used by custo.\n            this.el.querySelectorAll(\".navbar-collapse.show\").forEach(collapseEl => {\n                Collapse.getOrCreateInstance(collapseEl).hide();\n            });\n        }\n    },\n});\n\npublicWidget.registry.StandardAffixedHeader = BaseAnimatedHeader.extend({\n    selector: 'header.o_header_standard:not(.o_header_sidebar)',\n\n    /**\n     * @constructor\n     */\n    init: function () {\n        this._super(...arguments);\n        this.fixedHeaderShow = false;\n        this.scrolledPoint = 300;\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        headerHeight ||= this.el.getBoundingClientRect().height;\n        return this._super.apply(this, arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this.$el.css('transform', '');\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _isShown() {\n        return !this.fixedHeader || this.fixedHeaderShow;\n    },\n    /**\n     * Called when the window is scrolled\n     *\n     * @override\n     * @param {integer} scroll\n     */\n    _updateHeaderOnScroll: function (scroll) {\n        this._super(...arguments);\n\n        const mainPosScrolled = (scroll > headerHeight + this.topGap);\n        const reachPosScrolled = (scroll > this.scrolledPoint + this.topGap) && !this.scrollHeightTooShort;\n        const fixedUpdate = (this.fixedHeader !== mainPosScrolled);\n        const showUpdate = (this.fixedHeaderShow !== reachPosScrolled);\n\n        if (fixedUpdate || showUpdate) {\n            this.$el.css('transform',\n                reachPosScrolled\n                ? `translate(0, -${this.topGap}px)`\n                : mainPosScrolled\n                ? 'translate(0, -100%)'\n                : '');\n            void this.$el[0].offsetWidth; // Force a paint refresh\n        }\n\n        this.fixedHeaderShow = reachPosScrolled;\n        this.hiddenOnScrollEl?.classList.toggle(\"hidden\", mainPosScrolled);\n\n        if (fixedUpdate) {\n            this._toggleFixedHeader(mainPosScrolled);\n        } else if (showUpdate) {\n            this._adaptToHeaderChange();\n        }\n    },\n});\n\npublicWidget.registry.FixedHeader = BaseAnimatedHeader.extend({\n    selector: 'header.o_header_fixed:not(.o_header_sidebar)',\n\n    /**\n     * @override\n     */\n    start() {\n        const _super = this._super(...arguments);\n        this.dropdownToggleEls = [];\n        if (this.hiddenOnScrollEl) {\n            this.dropdownToggleEls = this.hiddenOnScrollEl.querySelectorAll(\".dropdown-toggle\");\n            for (const dropdownToggleEl of this.dropdownToggleEls) {\n                this.__onDropdownShow = this._onDropdownShow.bind(this);\n                dropdownToggleEl.addEventListener(\"show.bs.dropdown\", this.__onDropdownShow);\n            }\n            this.searchbarEl = this.hiddenOnScrollEl\n                .querySelector(\":not(.modal-content) > .o_searchbar_form\");\n            if (this.searchbarEl) {\n                this.__onSearchbarInput = this._onSearchbarInput.bind(this);\n                this.searchbarEl.addEventListener(\"input\", this.__onSearchbarInput);\n            }\n        }\n        return _super;\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        for (const dropdownToggleEl of this.dropdownToggleEls) {\n            dropdownToggleEl.removeEventListener(\"show.bs.dropdown\", this.__onDropdownShow);\n        }\n        if (this.searchbarEl) {\n            this.searchbarEl.removeEventListener(\"input\", this.__onSearchbarInput);\n        }\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _updateHeaderOnScroll: function (scroll) {\n        this._super(...arguments);\n        // Need to be 'unfixed' when the window is not scrolled so that the\n        // transparent menu option still works.\n        if (scroll > (this.scrolledPoint + this.topGap)) {\n            if (!this.$el.hasClass('o_header_affixed')) {\n                this.$el.css('transform', `translate(0, -${this.topGap}px)`);\n                void this.$el[0].offsetWidth; // Force a paint refresh\n                this._toggleFixedHeader(true);\n            }\n        } else {\n            this._toggleFixedHeader(false);\n            void this.$el[0].offsetWidth; // Force a paint refresh\n            this.$el.css('transform', '');\n        }\n\n        if (this.hiddenOnScrollEl) {\n            let elHeight = 0;\n            if (this.fixedHeader && this.searchbarEl?.matches(\".show\")) {\n                // Close the dropdown of the search bar if it's open when\n                // scrolling. Otherwise, the calculated height of the\n                // 'hiddenOnScrollEl' element will be incorrect because it will\n                // include the dropdown height.\n                this.searchbarEl.querySelector(\"input\").blur();\n                elHeight = this.hiddenOnScrollEl.offsetHeight;\n            } else {\n                elHeight = this.hiddenOnScrollEl.scrollHeight;\n            }\n            const scrollDelta = window.matchMedia(`(prefers-reduced-motion: reduce)`).matches ?\n                scroll : Math.floor(scroll / 4);\n            elHeight = Math.max(0, elHeight - scrollDelta);\n            this.hiddenOnScrollEl.classList.toggle(\"hidden\", elHeight === 0);\n            if (elHeight === 0) {\n                this.hiddenOnScrollEl.removeAttribute(\"style\");\n            } else {\n                // When the page hasn't been scrolled yet, we don't set overflow\n                // to hidden. Without this, the dropdowns would be invisible.\n                // (e.g., \"user menu\" dropdown).\n                this.hiddenOnScrollEl.style.overflow = this.fixedHeader ? \"hidden\" : \"\";\n                this.hiddenOnScrollEl.style.height = this.fixedHeader ? `${elHeight}px` : \"\";\n                let elPadding = parseInt(getComputedStyle(this.hiddenOnScrollEl).paddingBlock);\n                if (elHeight < elPadding * 2) {\n                    const heightDifference = elPadding * 2 - elHeight;\n                    elPadding = Math.max(0, elPadding - Math.floor(heightDifference / 2));\n                    this.hiddenOnScrollEl.style\n                        .setProperty(\"padding-block\", `${elPadding}px`, \"important\");\n                } else {\n                    this.hiddenOnScrollEl.style.paddingBlock = \"\";\n                }\n                if (this.fixedHeader) {\n                    // The height of the \"hiddenOnScrollEl\" element changes, so\n                    // the height of the header also changes. Therefore, we need\n                    // to get the current height of the header and then to\n                    // update the top padding of the main element.\n                    headerHeight = this.el.getBoundingClientRect().height;\n                    this._updateMainPaddingTop();\n                }\n            }\n            if (!this.fixedHeader && this.dropdownClickedEl) {\n                const dropdown = Dropdown.getOrCreateInstance(this.dropdownClickedEl);\n                dropdown.show();\n                this.dropdownClickedEl = null;\n            }\n        }\n    },\n    /**\n     * Called when a dropdown within 'this.hiddenOnScrollEl' is clicked.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onDropdownShow(ev) {\n        // If a dropdown inside the element 'this.hiddenOnScrollEl' is clicked\n        // while the header is fixed, we need to scroll the page up so that the\n        // 'this.hiddenOnScrollEl' element is no longer overflow hidden. Without\n        // this, the dropdown would be invisible.\n        if (this.fixedHeader) {\n            ev.preventDefault();\n            this.scrollableEl.scrollTo({ top: 0, behavior: \"smooth\" });\n            this.dropdownClickedEl = ev.currentTarget;\n        }\n    },\n    /**\n     * Called when a searchbar within 'this.hiddenOnScrollEl' receives input.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onSearchbarInput(ev) {\n        // Prevents the dropdown with search results from being hidden when the\n        // header is fixed (see comment in '_onDropdownClick').\n        // The scroll animation is instantaneous because the dropdown could open\n        // before reaching the top of the page, which would result in an\n        // incorrect calculated height of the header.\n        if (this.fixedHeader) {\n            this.scrollableEl.scrollTo({ top: 0 });\n        }\n    },\n});\n\nconst BaseDisappearingHeader = publicWidget.registry.FixedHeader.extend({\n    /**\n     * @override\n     */\n    init: function () {\n        this._super(...arguments);\n        this.scrollingDownwards = true;\n        this.hiddenHeader = false;\n        this.position = 0;\n        this.atTop = true;\n        this.checkPoint = 0;\n        this.scrollOffsetLimit = 200;\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        this._showHeader();\n        this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _hideHeader: function () {\n        this.$el.trigger('odoo-transitionstart');\n    },\n    /**\n     * @override\n     */\n    _isShown() {\n        return !this.fixedHeader || !this.hiddenHeader;\n    },\n    /**\n     * @private\n     */\n    _showHeader: function () {\n        this.$el.trigger('odoo-transitionstart');\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _updateHeaderOnScroll: function (scroll) {\n        this._super(...arguments);\n\n        const scrollingDownwards = (scroll > this.position);\n        const atTop = (scroll <= 0);\n        if (scrollingDownwards !== this.scrollingDownwards) {\n            this.checkPoint = scroll;\n        }\n\n        this.scrollingDownwards = scrollingDownwards;\n        this.position = scroll;\n        this.atTop = atTop;\n\n        if (scrollingDownwards) {\n            if (!this.hiddenHeader && scroll - this.checkPoint > (this.scrollOffsetLimit + this.topGap)) {\n                this.hiddenHeader = true;\n                this._hideHeader();\n            }\n        } else {\n            if (this.hiddenHeader && scroll - this.checkPoint < -(this.scrollOffsetLimit + this.topGap) / 2) {\n                this.hiddenHeader = false;\n                this._showHeader();\n            }\n        }\n\n        if (atTop && !this.atTop) {\n            // Force reshowing the invisible-on-scroll sections when reaching\n            // the top again\n            this._showHeader();\n        }\n    },\n});\n\npublicWidget.registry.DisappearingHeader = BaseDisappearingHeader.extend({\n    selector: 'header.o_header_disappears:not(.o_header_sidebar)',\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _adjustUrlAutoScroll() {},\n    /**\n     * @override\n     */\n    _hideHeader: function () {\n        this._super(...arguments);\n        this.$el.css('transform', 'translate(0, -100%)');\n    },\n    /**\n     * @override\n     */\n    _showHeader: function () {\n        this._super(...arguments);\n        this.$el.css('transform', this.atTop ? '' : `translate(0, -${this.topGap}px)`);\n    },\n});\n\npublicWidget.registry.FadeOutHeader = BaseDisappearingHeader.extend({\n    selector: 'header.o_header_fade_out:not(.o_header_sidebar)',\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _adjustUrlAutoScroll() {},\n    /**\n     * @override\n     */\n    _hideHeader: function () {\n        this._super(...arguments);\n        this.$el.stop(false, true).fadeOut();\n    },\n    /**\n     * @override\n     */\n    _showHeader: function () {\n        this._super(...arguments);\n        this.$el.css('transform', this.atTop ? '' : `translate(0, -${this.topGap}px)`);\n        this.$el.stop(false, true).fadeIn();\n    },\n});\n\npublicWidget.registry.hoverableDropdown = animations.Animation.extend({\n    selector: 'header.o_hoverable_dropdown',\n    disabledInEditableMode: false,\n    effects: [{\n        startEvents: 'resize',\n        update: '_dropdownHover',\n    }],\n    events: {\n        'mouseenter .dropdown': '_onMouseEnter',\n        'mouseleave .dropdown': '_onMouseLeave',\n    },\n\n    /**\n     * @override\n     */\n    start: function () {\n        this.$dropdownMenus = this.$el.find('.dropdown-menu');\n        this.$dropdownToggles = this.$el.find('.dropdown-toggle');\n        this._dropdownHover();\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _dropdownHover: function () {\n        this.$dropdownMenus.attr('data-bs-popper', 'none');\n        if (uiUtils.getSize() >= SIZES.LG) {\n            this.$dropdownMenus.css('margin-top', '0');\n            this.$dropdownMenus.css('top', 'unset');\n        } else {\n            this.$dropdownMenus.css('margin-top', '');\n            this.$dropdownMenus.css('top', '');\n        }\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     * @param {boolean} [doShow=true] true to show, false to hide\n     */\n    _updateDropdownVisibility(ev, doShow = true) {\n        if (uiUtils.getSize() < SIZES.LG) {\n            return;\n        }\n        if (ev.currentTarget.closest('.o_extra_menu_items')) {\n            return;\n        }\n        const dropdownToggleEl = ev.currentTarget.querySelector('.dropdown-toggle');\n        if (!dropdownToggleEl) {\n            return;\n        }\n        const dropdown = Dropdown.getOrCreateInstance(dropdownToggleEl);\n        if (doShow) {\n            dropdown.show();\n        } else {\n            dropdown.hide();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onMouseEnter: function (ev) {\n        if (this.editableMode) {\n            // Do not handle hover if another dropdown is opened.\n            if (this.el.querySelector('.dropdown-toggle.show')) {\n                return;\n            }\n        }\n        // Get the previously focused element of the page.\n        const focusedEl = this.el.ownerDocument.querySelector(\":focus\")\n            || window.frameElement && window.frameElement.ownerDocument.querySelector(\":focus\");\n\n        // The user must click on the dropdown if he is on mobile (no way to\n        // hover) or if the dropdown is the (or in the) extra menu ('+').\n        this._updateDropdownVisibility(ev, true);\n\n        // Keep the focus on the previously focused element if any, otherwise do\n        // not focus the dropdown on hover.\n        if (focusedEl) {\n            focusedEl.focus({preventScroll: true});\n        } else {\n            const dropdownToggleEl = ev.currentTarget.querySelector(\".dropdown-toggle\");\n            if (dropdownToggleEl) {\n                dropdownToggleEl.blur();\n            }\n        }\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onMouseLeave: function (ev) {\n        if (this.editableMode) {\n            // Cancel handling from view mode.\n            return;\n        }\n        this._updateDropdownVisibility(ev, false);\n    },\n});\n\npublicWidget.registry.MegaMenuDropdown = publicWidget.Widget.extend({\n    selector: \"header#top\",\n    disabledInEditableMode: false,\n    events: {\n        \"mousedown .o_mega_menu_toggle\": \"_onMegaMenuClick\",\n        \"mouseenter .o_mega_menu_toggle\": \"_onMegaMenuHover\",\n        \"mousedown .o_extra_menu_items\": \"_onExtraMenuClick\",\n        \"keyup .o_mega_menu_toggle\": \"_onMegaMenuClick\",\n        \"keyup .o_extra_menu_items\": \"_onExtraMenuClick\",\n    },\n\n    /**\n     * @override\n     */\n    start() {\n        const toggleEls = this.el.querySelectorAll(\".o_mega_menu_toggle\");\n        this.desktopMegaMenuToggleEls = [];\n        this.mobileMegaMenuToggleEls = [];\n        for (const el of toggleEls) {\n            if (el.closest(\".o_header_mobile\")) {\n                this.mobileMegaMenuToggleEls.push(el);\n            } else {\n                this.desktopMegaMenuToggleEls.push(el);\n            }\n        }\n\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * If the mega menu dropdown on which we are clicking/hovering does not have\n     * a mega menu (i.e. it is in the other navbar), brings the corresponding\n     * mega menu into it.\n     *\n     * @private\n     * @param {Element} megaMenuToggleEl the clicked/hovered mega menu dropdown\n     */\n    _moveMegaMenu(megaMenuToggleEl) {\n        const hasMegaMenu = !!megaMenuToggleEl.parentElement.querySelector(\".o_mega_menu\");\n        if (hasMegaMenu) {\n            return;\n        }\n        this.options.wysiwyg?.odooEditor.observerUnactive(\"moveMegaMenu\");\n        const isMobileNavbar = !!megaMenuToggleEl.closest(\".o_header_mobile\");\n        const currentNavbarToggleEls = isMobileNavbar ?\n            this.mobileMegaMenuToggleEls : this.desktopMegaMenuToggleEls;\n        const otherNavbarToggleEls = isMobileNavbar ?\n            this.desktopMegaMenuToggleEls : this.mobileMegaMenuToggleEls;\n        const megaMenuToggleIndex = currentNavbarToggleEls.indexOf(megaMenuToggleEl);\n        const previousMegaMenuToggleEl = otherNavbarToggleEls[megaMenuToggleIndex];\n        const megaMenuEl = previousMegaMenuToggleEl.parentElement.querySelector(\".o_mega_menu\");\n        // Hiding the dropdown where the mega menu comes from before moving it,\n        // so everything is in a consistent state.\n        Dropdown.getOrCreateInstance(previousMegaMenuToggleEl).hide();\n        megaMenuToggleEl.insertAdjacentElement(\"afterend\", megaMenuEl);\n        this.options.wysiwyg?.odooEditor.observerActive(\"moveMegaMenu\");\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when a mega menu dropdown is clicked/key pressed.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onMegaMenuClick(ev) {\n        const megaMenuToggleEl = ev.currentTarget;\n        // Ignore the event if the menus are hoverable and in desktop view (the\n        // hoverable menus are clicked on mobile view), but not if we used the\n        // keyboard.\n        if (this.el.classList.contains(\"o_hoverable_dropdown\")\n                && !megaMenuToggleEl.closest(\".o_header_mobile\") && ev.type !== \"keyup\") {\n            return;\n        }\n        this._moveMegaMenu(megaMenuToggleEl);\n    },\n    /**\n     * Called when a mega menu dropdown is hovered.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onMegaMenuHover(ev) {\n        const megaMenuToggleEl = ev.currentTarget;\n        // Ignore the event if the menus are not hoverable or if we are in\n        // mobile view (again, the hoverable menus are clicked on mobile view).\n        if (!this.el.classList.contains(\"o_hoverable_dropdown\")\n                || megaMenuToggleEl.closest(\".o_header_mobile\")) {\n            return;\n        }\n        this._moveMegaMenu(megaMenuToggleEl);\n    },\n    /**\n     * Called when the extra menu (+) dropdown is clicked/key pressed.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onExtraMenuClick(ev) {\n        const megaMenuToggleEls = ev.currentTarget.querySelectorAll(\".o_mega_menu_toggle\");\n        megaMenuToggleEls.forEach(megaMenuToggleEl => this._moveMegaMenu(megaMenuToggleEl));\n    },\n});\n\npublicWidget.registry.HeaderGeneral = publicWidget.Widget.extend({\n    selector: 'header#top',\n    disabledInEditableMode: false,\n    events: {\n        \"show.bs.offcanvas #top_menu_collapse, #top_menu_collapse_mobile\": \"_onCollapseShow\",\n        \"hidden.bs.offcanvas #top_menu_collapse, #top_menu_collapse_mobile\": \"_onCollapseHidden\",\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onCollapseShow() {\n        this.options.wysiwyg?.odooEditor.observerUnactive(\"addCollapseClass\");\n        this.el.classList.add('o_top_menu_collapse_shown');\n        this.options.wysiwyg?.odooEditor.observerActive(\"addCollapseClass\");\n    },\n    /**\n     * @private\n     */\n    _onCollapseHidden() {\n        this.options.wysiwyg?.odooEditor.observerUnactive(\"removeCollapseClass\");\n        const mobileNavbarEl = this.el.querySelector(\"#top_menu_collapse_mobile\");\n        if (!mobileNavbarEl.matches(\".show, .showing\")) {\n            this.el.classList.remove(\"o_top_menu_collapse_shown\");\n        }\n        this.options.wysiwyg?.odooEditor.observerActive(\"removeCollapseClass\");\n    },\n});\n\npublicWidget.registry.SearchModal = publicWidget.Widget.extend({\n    selector: \"#o_search_modal_block #o_search_modal\",\n    disabledInEditableMode: false,\n    events: {\n        \"show.bs.modal\": \"_onSearchModalShow\",\n        \"shown.bs.modal\": \"_onSearchModalShown\",\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onSearchModalShow(ev) {\n        if (this.editableMode) {\n            ev.preventDefault();\n        }\n    },\n    /**\n     * @private\n     */\n    _onSearchModalShown(ev) {\n        this.el.querySelector(\".search-query\").focus();\n    },\n});\n\nexport default {\n    extraMenuUpdateCallbacks: extraMenuUpdateCallbacks,\n};\n", "/** @odoo-module **/\n\n/**\n * Provides a way to start JS code for snippets' initialization and animations.\n */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { loadJS } from \"@web/core/assets\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { debounce, throttleForAnimation } from \"@web/core/utils/timing\";\nimport Class from \"@web/legacy/js/core/class\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport wUtils from \"@website/js/utils\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport { SIZES, utils as uiUtils } from \"@web/core/ui/ui_service\";\nimport {\n    applyTextHighlight,\n    removeTextHighlight,\n    switchTextHighlight,\n} from \"@website/js/text_processing\";\nimport { touching } from \"@web/core/utils/ui\";\nimport { ObservingCookieWidgetMixin } from \"@website/snippets/observing_cookie_mixin\";\nimport { scrollTo } from \"@web_editor/js/common/scrolling\";\n\n// Initialize fallbacks for the use of requestAnimationFrame,\n// cancelAnimationFrame and performance.now()\nwindow.requestAnimationFrame = window.requestAnimationFrame\n    || window.webkitRequestAnimationFrame\n    || window.mozRequestAnimationFrame\n    || window.msRequestAnimationFrame\n    || window.oRequestAnimationFrame;\nwindow.cancelAnimationFrame = window.cancelAnimationFrame\n    || window.webkitCancelAnimationFrame\n    || window.mozCancelAnimationFrame\n    || window.msCancelAnimationFrame\n    || window.oCancelAnimationFrame;\nif (!window.performance || !window.performance.now) {\n    window.performance = {\n        now: function () {\n            return Date.now();\n        }\n    };\n}\n\n/**\n * Add the notion of edit mode to public widgets.\n */\npublicWidget.Widget.include({\n    /**\n     * Indicates if the widget should not be instantiated in edit. The default\n     * is true, indeed most (all?) defined widgets only want to initialize\n     * events and states which should not be active in edit mode (this is\n     * especially true for non-website widgets).\n     *\n     * @type {boolean}\n     */\n    disabledInEditableMode: true,\n    /**\n     * Acts as @see Widget.events except that the events are only binded if the\n     * Widget instance is instanciated in edit mode. The property is not\n     * considered if @see disabledInEditableMode is false.\n     */\n    edit_events: null,\n    /**\n     * Acts as @see Widget.events except that the events are only binded if the\n     * Widget instance is instanciated in readonly mode. The property only\n     * makes sense if @see disabledInEditableMode is false, you should simply\n     * use @see Widget.events otherwise.\n     */\n    read_events: null,\n\n    /**\n     * Initializes the events that will need to be binded according to the\n     * given mode.\n     *\n     * @constructor\n     * @param {Object} parent\n     * @param {Object} [options]\n     * @param {boolean} [options.editableMode=false]\n     *        true if the page is in edition mode\n     */\n    init: function (parent, options) {\n        this._super.apply(this, arguments);\n\n        this.editableMode = this.options.editableMode || false;\n        var extraEvents = this.editableMode ? this.edit_events : this.read_events;\n        if (extraEvents) {\n            this.events = Object.assign({}, this.events || {}, extraEvents);\n        }\n    },\n});\n\n/**\n * In charge of handling one animation loop using the requestAnimationFrame\n * feature. This is used by the `Animation` class below and should not be called\n * directly by an end developer.\n *\n * This uses a simple API: it can be started, stopped, played and paused.\n */\nvar AnimationEffect = Class.extend(publicWidget.ParentedMixin, {\n    /**\n     * @constructor\n     * @param {Object} parent\n     * @param {function} updateCallback - the animation update callback\n     * @param {string} [startEvents=scroll]\n     *        space separated list of events which starts the animation loop\n     * @param {jQuery|DOMElement} [$startTarget=window]\n     *        the element(s) on which the startEvents are listened\n     * @param {Object} [options]\n     * @param {function} [options.getStateCallback]\n     *        a function which returns a value which represents the state of the\n     *        animation, i.e. for two same value, no refreshing of the animation\n     *        is needed. Can be used for optimization. If the $startTarget is\n     *        the window element, this defaults to returning the current\n     *        scoll offset of the window or the size of the window for the\n     *        scroll and resize events respectively.\n     * @param {string} [options.endEvents]\n     *        space separated list of events which pause the animation loop. If\n     *        not given, the animation is stopped after a while (if no\n     *        startEvents is received again)\n     * @param {jQuery|DOMElement} [options.$endTarget=$startTarget]\n     *        the element(s) on which the endEvents are listened\n     * @param {boolean} [options.enableInModal]\n     *        when it is true, it means that the 'scroll' event must be\n     *        triggered when scrolling a modal.\n     */\n    init: function (parent, updateCallback, startEvents, $startTarget, options) {\n        publicWidget.ParentedMixin.init.call(this);\n        this.setParent(parent);\n\n        options = options || {};\n        this._minFrameTime = 1000 / (options.maxFPS || 100);\n\n        // Initialize the animation startEvents, startTarget, endEvents, endTarget and callbacks\n        this._updateCallback = updateCallback;\n        this.startEvents = startEvents || 'scroll';\n        const modalEl = options.enableInModal ? parent.target.closest('.modal') : null;\n        const mainScrollingElement = modalEl ? modalEl : $().getScrollingElement()[0];\n        const mainScrollingTarget = $().getScrollingTarget(mainScrollingElement)[0];\n        this.$startTarget = $($startTarget ? $startTarget : this.startEvents === 'scroll' ? mainScrollingTarget : window);\n        if (options.getStateCallback) {\n            this._getStateCallback = options.getStateCallback;\n        } else if (this.startEvents === 'scroll' && this.$startTarget[0] === mainScrollingTarget) {\n            const $scrollable = this.$startTarget;\n            this._getStateCallback = function () {\n                return $scrollable.scrollTop();\n            };\n        } else if (this.startEvents === 'resize' && this.$startTarget[0] === window) {\n            this._getStateCallback = function () {\n                return {\n                    width: window.innerWidth,\n                    height: window.innerHeight,\n                };\n            };\n        } else {\n            this._getStateCallback = function () {\n                return undefined;\n            };\n        }\n        this.endEvents = options.endEvents || false;\n        this.$endTarget = options.$endTarget ? $(options.$endTarget) : this.$startTarget;\n\n        this._updateCallback = this._updateCallback.bind(parent);\n        this._getStateCallback = this._getStateCallback.bind(parent);\n\n        // Add a namespace to events using the generated uid\n        this._uid = uniqueId(\"_animationEffect\");\n        this.startEvents = _processEvents(this.startEvents, this._uid);\n        if (this.endEvents) {\n            this.endEvents = _processEvents(this.endEvents, this._uid);\n        }\n\n        function _processEvents(events, namespace) {\n            return events\n                .split(\" \")\n                .map((e) => (e += \".\" + namespace))\n                .join(\" \");\n        }\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        publicWidget.ParentedMixin.destroy.call(this);\n        this.stop();\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Initializes when the animation must be played and paused and initializes\n     * the animation first frame.\n     */\n    start: function () {\n        // Initialize the animation first frame\n        this._paused = false;\n        this._rafID = window.requestAnimationFrame((function (t) {\n            this._update(t);\n            this._paused = true;\n        }).bind(this));\n\n        // Initialize the animation play/pause events\n        if (this.endEvents) {\n            /**\n             * If there are endEvents, the animation should begin playing when\n             * the startEvents are triggered on the $startTarget and pause when\n             * the endEvents are triggered on the $endTarget.\n             */\n            this.$startTarget.on(this.startEvents, (function (e) {\n                if (this._paused) {\n                    setTimeout(() => this.play.bind(this, e));\n                }\n            }).bind(this));\n            this.$endTarget.on(this.endEvents, (function () {\n                if (!this._paused) {\n                    setTimeout(() => this.pause.bind(this));\n                }\n            }).bind(this));\n        } else {\n            /**\n             * Else, if there is no endEvents, the animation should begin playing\n             * when the startEvents are *continuously* triggered on the\n             * $startTarget or fully played once. To achieve this, the animation\n             * begins playing and is scheduled to pause after 2 seconds. If the\n             * startEvents are triggered during that time, this is not paused\n             * for another 2 seconds. This allows to describe an \"effect\"\n             * animation (which lasts less than 2 seconds) or an animation which\n             * must be playing *during* an event (scroll, mousemove, resize,\n             * repeated clicks, ...).\n             */\n            this.throttleOnStartEvents = throttleForAnimation(\n                ((e) => {\n                    this.play(e);\n                    clearTimeout(pauseTimer);\n                    pauseTimer = setTimeout(\n                        (() => {\n                            this.pause();\n                            pauseTimer = null;\n                        }).bind(this),\n                        2000\n                    );\n                }).bind(this)\n            );\n            var pauseTimer = null;\n            this.$startTarget.on(this.startEvents, this.throttleOnStartEvents);\n        }\n    },\n    /**\n     * Pauses the animation and destroys the attached events which trigger the\n     * animation to be played or paused.\n     */\n    stop: function () {\n        this.$startTarget.off(this.startEvents);\n        if (this.endEvents) {\n            this.$endTarget.off(this.endEvents);\n        }\n        this.pause();\n    },\n    /**\n     * Forces the requestAnimationFrame loop to start.\n     *\n     * @param {Event} e - the event which triggered the animation to play\n     */\n    play: function (e) {\n        this._newEvent = e;\n        if (!this._paused) {\n            return;\n        }\n        this._paused = false;\n        this._rafID = window.requestAnimationFrame(this._update.bind(this));\n        this._lastUpdateTimestamp = undefined;\n    },\n    /**\n     * Forces the requestAnimationFrame loop to stop.\n     */\n    pause: function () {\n        if (this._paused) {\n            return;\n        }\n        this._paused = true;\n        window.cancelAnimationFrame(this._rafID);\n        this._lastUpdateTimestamp = undefined;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Callback which is repeatedly called by the requestAnimationFrame loop.\n     * It controls the max fps at which the animation is running and initializes\n     * the values that the update callback needs to describe the animation\n     * (state, elapsedTime, triggered event).\n     *\n     * @private\n     * @param {DOMHighResTimeStamp} timestamp\n     */\n    _update: function (timestamp) {\n        if (this._paused) {\n            return;\n        }\n        this._rafID = window.requestAnimationFrame(this._update.bind(this));\n\n        // Check the elapsed time since the last update callback call.\n        // Consider it 0 if there is no info of last timestamp and leave this\n        // _update call if it was called too soon (would overflow the set max FPS).\n        var elapsedTime = 0;\n        if (this._lastUpdateTimestamp) {\n            elapsedTime = timestamp - this._lastUpdateTimestamp;\n            if (elapsedTime < this._minFrameTime) {\n                return;\n            }\n        }\n\n        // Check the new animation state thanks to the get state callback and\n        // store its new value. If the state is the same as the previous one,\n        // leave this _update call, except if there is an event which triggered\n        // the \"play\" method again.\n        var animationState = this._getStateCallback(elapsedTime, this._newEvent);\n        if (!this._newEvent\n         && animationState !== undefined\n         && JSON.stringify(animationState) === JSON.stringify(this._animationLastState)) {\n            return;\n        }\n        this._animationLastState = animationState;\n\n        // Call the update callback with frame parameters\n        this._updateCallback(this._animationLastState, elapsedTime, this._newEvent);\n        this._lastUpdateTimestamp = timestamp; // Save the timestamp at which the update callback was really called\n        this._newEvent = undefined; // Forget the event which triggered the last \"play\" call\n    },\n});\n\n/**\n * Also register AnimationEffect automatically (@see effects, _prepareEffects).\n */\nvar Animation = publicWidget.Widget.extend({\n    /**\n     * The max FPS at which all the automatic animation effects will be\n     * running by default.\n     */\n    maxFPS: 100,\n    /**\n     * @see this._prepareEffects\n     *\n     * @type {Object[]}\n     * @type {string} startEvents\n     *       The names of the events which trigger the effect to begin playing.\n     * @type {string} [startTarget]\n     *       A selector to find the target where to listen for the start events\n     *       (if no selector, the window target will be used). If the whole\n     *       element of the animation should be used, use the 'selector' string.\n     * @type {string} [endEvents]\n     *       The name of the events which trigger the end of the effect (if none\n     *       is defined, the animation will stop after a while\n     *       @see AnimationEffect.start).\n     * @type {string} [endTarget]\n     *       A selector to find the target where to listen for the end events\n     *       (if no selector, the startTarget will be used). If the whole\n     *       element of the animation should be used, use the 'selector' string.\n     * @type {string} update\n     *       A string which refers to a method which will be used as the update\n     *       callback for the effect. It receives 3 arguments: the animation\n     *       state, the elapsedTime since last update and the event which\n     *       triggered the animation (undefined if just a new update call\n     *       without trigger).\n     * @type {string} [getState]\n     *       The animation state is undefined by default, the scroll offset for\n     *       the particular {startEvents: 'scroll'} effect and an object with\n     *       width and height for the particular {startEvents: 'resize'} effect.\n     *       There is the possibility to define the getState callback of the\n     *       animation effect with this key. This allows to improve performance\n     *       even further in some cases.\n     */\n    effects: [],\n\n    /**\n     * Initializes the animation. The method should not be called directly as\n     * called automatically on animation instantiation and on restart.\n     *\n     * Also, prepares animation's effects and start them if any.\n     *\n     * @override\n     */\n    start: function () {\n        this._prepareEffects();\n        this._animationEffects.forEach((effect) => {\n            effect.start();\n        });\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Registers `AnimationEffect` instances.\n     *\n     * This can be done by extending this method and calling the @see _addEffect\n     * method in it or, better, by filling the @see effects property.\n     *\n     * @private\n     */\n    _prepareEffects: function () {\n        this._animationEffects = [];\n\n        var self = this;\n        this.effects.forEach((desc) => {\n            self._addEffect(self[desc.update], desc.startEvents, _findTarget(desc.startTarget), {\n                getStateCallback: desc.getState && self[desc.getState],\n                endEvents: desc.endEvents || undefined,\n                $endTarget: _findTarget(desc.endTarget),\n                maxFPS: self.maxFPS,\n                enableInModal: desc.enableInModal || undefined,\n            });\n\n            // Return the DOM element matching the selector in the form\n            // described above.\n            function _findTarget(selector) {\n                if (selector) {\n                    if (selector === 'selector') {\n                        return self.$el;\n                    }\n                    return self.$(selector);\n                }\n                return undefined;\n            }\n        });\n    },\n    /**\n     * Registers a new `AnimationEffect` according to given parameters.\n     *\n     * @private\n     * @see AnimationEffect.init\n     */\n    _addEffect: function (updateCallback, startEvents, $startTarget, options) {\n        this._animationEffects.push(\n            new AnimationEffect(this, updateCallback, startEvents, $startTarget, options)\n        );\n    },\n});\n\n//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\n\nvar registry = publicWidget.registry;\n\n// FIXME temporary hack: during edit mode, the carousel crashes sometimes when\n// we hover option during a carousel cycle. This patches Bootstrap to prevent\n// the crash.\nconst baseSelectorEngineFind = window.SelectorEngine.find;\nwindow.SelectorEngine.find = function (...args) {\n    try {\n        return baseSelectorEngineFind.call(this, ...args);\n    } catch {\n        return [document.createElement('div')];\n    }\n};\n\nregistry.slider = publicWidget.Widget.extend({\n    selector: '.carousel',\n    disabledInEditableMode: false,\n    edit_events: {\n        'content_changed': '_onContentChanged',\n    },\n\n    /**\n     * @override\n     */\n    start: function () {\n        this.$('img').on('load.slider', () => this._computeHeights());\n        this._computeHeights();\n        $(window).on('resize.slider', debounce(() => this._computeHeights(), 250));\n\n        // Initialize carousel and pause if in edit mode.\n        const options = this.editableMode ? {ride: false, pause: true} : undefined;\n        window.Carousel.getOrCreateInstance(this.el, options);\n\n        if (this.editableMode) {\n            // Prevent carousel slide to be an history step.\n            this.$el.on(\"slide.bs.carousel.slider\", () => {\n                this.options.wysiwyg.odooEditor.observerUnactive();\n            });\n            this.$el.on(\"slid.bs.carousel.slider\", () => {\n                this.options.wysiwyg.odooEditor.observerActive();\n            });\n        }\n        return this._super.apply(this, arguments);\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        this._super.apply(this, arguments);\n\n        window.Carousel.getOrCreateInstance(this.el).dispose();\n\n        this.$(\".carousel-item\")\n            .toArray()\n            .forEach((el) => {\n                $(el).css(\"min-height\", \"\");\n            });\n\n        $(window).off('.slider');\n        this.$el.off('.slider');\n        this.$('img').off('.slider');\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _computeHeights: function () {\n        var maxHeight = 0;\n        var $items = this.$('.carousel-item');\n        $items.css('min-height', '');\n        $items.toArray().forEach((el) => {\n            var $item = $(el);\n            var isActive = $item.hasClass('active');\n            this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerUnactive('_computeHeights');\n            $item.addClass('active');\n            this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerActive('_computeHeights');\n            var height = $item.outerHeight();\n            if (height > maxHeight) {\n                maxHeight = height;\n            }\n            this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerUnactive('_computeHeights');\n            $item.toggleClass('active', isActive);\n            this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerActive('_computeHeights');\n        });\n        $items.css('min-height', maxHeight);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onContentChanged: function (ev) {\n        this._computeHeights();\n    },\n});\n\nconst CAROUSEL_SLIDING_CLASS = \"o_carousel_sliding\";\n\n/**\n * @param {HTMLElement} carouselEl\n * @returns {Promise<void>}\n */\nasync function waitForCarouselToFinishSliding(carouselEl) {\n    if (!carouselEl.classList.contains(CAROUSEL_SLIDING_CLASS)) {\n        return;\n    }\n    return new Promise(resolve => {\n        carouselEl.addEventListener(\"slid.bs.carousel\", () => resolve(), {once: true});\n    });\n}\n\n/**\n * This class is used to fix carousel auto-slide behavior in Odoo 17.4 and up.\n * It handles upgrade cases from lower versions.\n * TODO find a way to get rid of this with an upgrade script?\n */\npublicWidget.registry.CarouselBootstrapUpgradeFix = publicWidget.Widget.extend({\n    // Only consider our known carousel snippets. A bootstrap carousel could\n    // have been added in an embed code snippet, or in any custom snippet. In\n    // that case, we consider that it should use the new default BS behavior,\n    // assuming the user / the developer of the custo should have updated the\n    // behavior as wanted themselves.\n    // Note: dynamic snippets are handled separately (TODO review).\n    selector: [\n        \"[data-snippet='s_image_gallery'] .carousel\",\n        \"[data-snippet='s_carousel'] .carousel\",\n        \"[data-snippet='s_quotes_carousel'] .carousel\",\n        \"[data-snippet='s_quotes_carousel_minimal'] .carousel\",\n        \"[data-snippet='s_carousel_intro'] .carousel\",\n    ].join(\", \"),\n    disabledInEditableMode: false,\n    events: {\n        \"slide.bs.carousel\": \"_onSlideCarousel\",\n        \"slid.bs.carousel\": \"_onSlidCarousel\",\n    },\n    OLD_AUTO_SLIDING_SNIPPETS: [\"s_image_gallery\"],\n\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n\n        const hasInterval = ![undefined, \"false\", \"0\"].includes(this.el.dataset.bsInterval);\n        if (!hasInterval && this.el.dataset.bsRide) {\n            // A bsInterval of 0 (or false or undefined) is intended to not\n            // auto-slide. With current Bootstrap version, a value of 0 will\n            // mean auto-slide without any delay (very fast). To prevent this,\n            // we remove the bsRide.\n            delete this.el.dataset.bsRide;\n            await this._destroyCarouselInstance();\n            const options = this.editableMode ? {ride: false, pause: true} : undefined;\n            window.Carousel.getOrCreateInstance(this.el, options);\n        } else if (hasInterval && !this.el.dataset.bsRide) {\n            // Re-add bsRide on carousels that don't have it but still have\n            // a bsInterval. E.g. s_image_gallery must auto-slide on load,\n            // while others only auto-slide on mouseleave.\n            //\n            // In the case of s_image_gallery that has a bsRide = \"true\"\n            // instead of \"carousel\", it's better not to change the behavior and\n            // let the user update the snippet manually to avoid making changes\n            // that they don't expect.\n            const snippetName = this.el.closest(\"[data-snippet]\").dataset.snippet;\n            this.el.dataset.bsRide = this.OLD_AUTO_SLIDING_SNIPPETS.includes(snippetName) ? \"carousel\" : \"true\";\n            await this._destroyCarouselInstance();\n            const options = this.editableMode ? {ride: false, pause: true} : undefined;\n            window.Carousel.getOrCreateInstance(this.el, options);\n        }\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        this.el.classList.remove(CAROUSEL_SLIDING_CLASS);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    async _destroyCarouselInstance() {\n        await waitForCarouselToFinishSliding(this.el); // Prevent traceback\n        window.Carousel.getInstance(this.el)?.dispose();\n    },\n    /**\n     * @private\n     */\n    _onSlideCarousel(ev) {\n        ev.currentTarget.classList.add(CAROUSEL_SLIDING_CLASS);\n    },\n    /**\n     * @private\n     */\n    _onSlidCarousel(ev) {\n        ev.currentTarget.classList.remove(CAROUSEL_SLIDING_CLASS);\n    },\n});\n\nregistry.Parallax = Animation.extend({\n    selector: '.parallax',\n    disabledInEditableMode: false,\n    effects: [{\n        startEvents: 'scroll',\n        update: '_onWindowScroll',\n        enableInModal: true,\n    }],\n\n    /**\n     * @override\n     */\n    start: function () {\n        this._rebuild();\n        $(window).on('resize.animation_parallax', debounce(this._rebuild.bind(this), 500));\n        this.modalEl = this.$target[0].closest('.modal');\n        if (this.modalEl) {\n            $(this.modalEl).on('shown.bs.modal.animation_parallax', () => {\n                this._rebuild();\n                this.modalEl.dispatchEvent(new Event('scroll'));\n            });\n        }\n        return this._super.apply(this, arguments);\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        this._super.apply(this, arguments);\n        this._updateBgCss({\n            transform: '',\n            top: '',\n            bottom: '',\n        });\n\n        $(window).off('.animation_parallax');\n        if (this.modalEl) {\n            $(this.modalEl).off('.animation_parallax');\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Prepares the background element which will scroll at a different speed\n     * according to the viewport dimensions and other snippet parameters.\n     *\n     * @private\n     */\n    _rebuild: function () {\n        // Add/find bg DOM element to hold the parallax bg (support old v10.0 parallax)\n        this.$bg = this.$('> .s_parallax_bg');\n\n        // Get parallax speed\n        this.speed = parseFloat(this.$el.attr('data-scroll-background-ratio') || 0);\n\n        // Reset offset if parallax effect will not be performed and leave\n        var noParallaxSpeed = (this.speed === 0 || this.speed === 1);\n        if (noParallaxSpeed) {\n            return;\n        }\n\n        // Initialize parallax data according to snippet and viewport dimensions\n        this.viewport = document.body.clientHeight - $('#wrapwrap').position().top;\n        this.visibleArea = [this.$el.offset().top];\n        this.visibleArea.push(this.visibleArea[0] + this.$el.innerHeight() + this.viewport);\n        this.ratio = this.speed * (this.viewport / 10);\n\n        // Provide a \"safe-area\" to limit parallax\n        const absoluteRatio = Math.abs(this.ratio);\n        this._updateBgCss({\n            top: -absoluteRatio,\n            bottom: -absoluteRatio,\n        });\n    },\n    /**\n     * Updates the parallax background element style with the provided CSS\n     * values.\n     * If the editor is enabled, it deactivates the observer during the CSS\n     * update.\n     *\n     * @param {Object} cssValues - The CSS values to apply to the background.\n     */\n    _updateBgCss(cssValues) {\n        if (!this.$bg) {\n            // Safety net in case the `destroy` is called before the `start` is\n            // executed.\n            return;\n        }\n        if (this.options.wysiwyg) {\n            this.options.wysiwyg.odooEditor.observerUnactive('_updateBgCss');\n        }\n        this.$bg.css(cssValues);\n        if (this.options.wysiwyg) {\n            this.options.wysiwyg.odooEditor.observerActive('_updateBgCss');\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Effects\n    //--------------------------------------------------------------------------\n\n    /**\n     * Describes how to update the snippet when the window scrolls.\n     *\n     * @private\n     * @param {integer} scrollOffset\n     */\n    _onWindowScroll: function (scrollOffset) {\n        // Speed == 0 is no effect and speed == 1 is handled by CSS only\n        if (this.speed === 0 || this.speed === 1) {\n            return;\n        }\n\n        // Perform translation if the element is visible only\n        var vpEndOffset = scrollOffset + this.viewport;\n        if (vpEndOffset >= this.visibleArea[0]\n         && vpEndOffset <= this.visibleArea[1]) {\n            this._updateBgCss({'transform': 'translateY(' + _getNormalizedPosition.call(this, vpEndOffset) + 'px)'});\n        }\n\n        function _getNormalizedPosition(pos) {\n            // Normalize scroll in a 1 to 0 range\n            var r = (pos - this.visibleArea[1]) / (this.visibleArea[0] - this.visibleArea[1]);\n            // Normalize accordingly to current options\n            return Math.round(this.ratio * (2 * r - 1));\n        }\n    },\n});\n\nconst MobileYoutubeAutoplayMixin = {\n    /**\n     * Takes care of any necessary setup for autoplaying video. In practice,\n     * this method will load the youtube iframe API for mobile environments\n     * because mobile environments don't support the youtube autoplay param\n     * passed in the url.\n     *\n     * @private\n     * @param {string} src - The source url of the video\n     */\n    _setupAutoplay: function (src) {\n        let promise = Promise.resolve();\n\n        this.isYoutubeVideo = src.indexOf('youtube') >= 0;\n        this.isMobileEnv = uiUtils.getSize() <= SIZES.LG && hasTouch();\n\n        if (this.isYoutubeVideo && this.isMobileEnv && !window.YT && !this.el.dataset.needCookiesApproval) {\n            const oldOnYoutubeIframeAPIReady = window.onYouTubeIframeAPIReady;\n            promise = new Promise(resolve => {\n                window.onYouTubeIframeAPIReady = () => {\n                    if (oldOnYoutubeIframeAPIReady) {\n                        oldOnYoutubeIframeAPIReady();\n                    }\n                    return resolve();\n                };\n            });\n            loadJS('https://www.youtube.com/iframe_api');\n        }\n\n        return promise;\n    },\n    /**\n     * @private\n     * @param {DOMElement} iframeEl - the iframe containing the video player\n     */\n    _triggerAutoplay: function (iframeEl) {\n        // YouTube does not allow to auto-play video in mobile devices, so we\n        // have to play the video manually.\n        if (this.isMobileEnv && this.isYoutubeVideo && !this.el.dataset.needCookiesApproval) {\n            new window.YT.Player(iframeEl, {\n                events: {\n                    onReady: ev => ev.target.playVideo(),\n                }\n            });\n        }\n    },\n};\n\nregistry.mediaVideo = publicWidget.Widget.extend(\n    MobileYoutubeAutoplayMixin, ObservingCookieWidgetMixin, {\n    selector: '.media_iframe_video',\n\n    /**\n     * @override\n     */\n    start: function () {\n        // TODO: this code should be refactored to make more sense and be better\n        // integrated with Odoo (this refactoring should be done in master).\n\n        const proms = [this._super.apply(this, arguments)];\n        let iframeEl = this.el.querySelector(':scope > iframe');\n\n        // The following code is only there to ensure compatibility with\n        // videos added before bug fixes or new Odoo versions where the\n        // <iframe/> element is properly saved.\n        if (!iframeEl) {\n            iframeEl = this._generateIframe();\n        }\n\n        if (this.el.dataset.needCookiesApproval) {\n            const sizeContainerEl = this.el.querySelector(\":scope > .media_iframe_video_size\");\n            sizeContainerEl.classList.add(\"d-none\");\n            this._showSizeContainerEl = () => {\n                sizeContainerEl.classList.remove(\"d-none\");\n            };\n            document.addEventListener(\n                \"optionalCookiesAccepted\", this._showSizeContainerEl, { once: true }\n            );\n        }\n\n        // We don't want to cause an error that would prevent entering edit mode\n        // if there is an iframe that doesn't have a src (this was possible for\n        // a while with the media dialog).\n        if (!iframeEl || !iframeEl.getAttribute('src')) {\n            // Something went wrong: no iframe is present in the DOM and the\n            // widget was unable to create one on the fly.\n            return Promise.all(proms);\n        }\n\n        proms.push(this._setupAutoplay(iframeEl.getAttribute('src')));\n        return Promise.all(proms).then(() => {\n            this._triggerAutoplay(iframeEl);\n        });\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        if (this._showSizeContainerEl) {\n            document.removeEventListener(\"optionalCookiesAccepted\", this._showSizeContainerEl);\n            this._showSizeContainerEl();\n        }\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _generateIframe: function () {\n        // Bug fix / compatibility: empty the <div/> element as all information\n        // to rebuild the iframe should have been saved on the <div/> element\n        this.$el.empty();\n\n        // Add extra content for size / edition\n        this.$el.append(\n            '<div class=\"css_editable_mode_display\">&nbsp;</div>' +\n            '<div class=\"media_iframe_video_size\">&nbsp;</div>'\n        );\n\n        // Rebuild the iframe. Depending on version / compatibility / instance,\n        // the src is saved in the 'data-src' attribute or the\n        // 'data-oe-expression' one (the latter is used as a workaround in 10.0\n        // system but should obviously be reviewed in master).\n        var src = escape(this.$el.data('oe-expression') || this.$el.data('src'));\n        // Validate the src to only accept supported domains we can trust\n        var m = src.match(/^(?:https?:)?\\/\\/([^/?#]+)/);\n        if (!m) {\n            // Unsupported protocol or wrong URL format, don't inject iframe\n            return;\n        }\n        var domain = m[1].replace(/^www\\./, '');\n        const supportedDomains = [\n            \"youtu.be\", \"youtube.com\", \"youtube-nocookie.com\",\n            \"instagram.com\",\n            \"player.vimeo.com\", \"vimeo.com\",\n            \"dailymotion.com\",\n            \"player.youku.com\", \"youku.com\",\n        ];\n        if (!supportedDomains.includes(domain)) {\n            // Unsupported domain, don't inject iframe\n            return;\n        }\n\n        const iframeEl = $('<iframe/>', {\n            frameborder: '0',\n            allowfullscreen: 'allowfullscreen',\n            \"aria-label\": _t(\"Media video\"),\n        })[0];\n        this.$el.append(iframeEl);\n        this._manageIframeSrc(this.el, src);\n        return iframeEl;\n    },\n});\n\nregistry.backgroundVideo = publicWidget.Widget.extend(MobileYoutubeAutoplayMixin, {\n    selector: '.o_background_video',\n    disabledInEditableMode: false,\n\n    /**\n     * @override\n     */\n    start: function () {\n        var proms = [this._super(...arguments)];\n\n        this.videoSrc = this.el.dataset.bgVideoSrc;\n        this.iframeID = uniqueId(\"o_bg_video_iframe_\");\n        proms.push(this._setupAutoplay(this.videoSrc));\n        if (this.isYoutubeVideo && this.isMobileEnv && !this.videoSrc.includes('enablejsapi=1')) {\n            // Compatibility: when choosing an autoplay youtube video via the\n            // media manager, the API was not automatically enabled before but\n            // only enabled here in the case of background videos.\n            // TODO migrate those old cases so this code can be removed?\n            this.videoSrc += '&enablejsapi=1';\n        }\n\n        this.throttledUpdate = throttleForAnimation(() => this._adjustIframe());\n\n        var $dropdownMenu = this.$el.closest('.dropdown-menu');\n        if ($dropdownMenu.length) {\n            this.$dropdownParent = $dropdownMenu.parent();\n            this.$dropdownParent.on(\"shown.bs.dropdown.backgroundVideo\", this.throttledUpdate);\n        }\n\n        $(window).on(\"resize.\" + this.iframeID, this.throttledUpdate);\n\n        const $modal = this.$el.closest('.modal');\n        if ($modal.length) {\n            $modal.on('show.bs.modal', () => {\n                const videoContainerEl = this.el.querySelector('.o_bg_video_container');\n                videoContainerEl.classList.add('d-none');\n            });\n            $modal.on('shown.bs.modal', () => {\n                this._adjustIframe();\n                const videoContainerEl = this.el.querySelector('.o_bg_video_container');\n                videoContainerEl.classList.remove('d-none');\n            });\n        }\n        return Promise.all(proms).then(() => this._appendBgVideo());\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        this._super.apply(this, arguments);\n\n        if (this.$dropdownParent) {\n            this.$dropdownParent.off('.backgroundVideo');\n        }\n\n        $(window).off('resize.' + this.iframeID);\n\n        this.throttledUpdate.cancel();\n\n        if (this.$bgVideoContainer) {\n            this.$bgVideoContainer.remove();\n        }\n        document.removeEventListener(\n            \"optionalCookiesAccepted\",\n            this.__onEnableVideoPostCookiesAccepted\n        );\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Adjusts iframe sizes and position so that it fills the container and so\n     * that it is centered in it.\n     *\n     * @private\n     */\n    _adjustIframe: function () {\n        if (!this.$iframe) {\n            return;\n        }\n\n        this.$iframe.removeClass('show');\n\n        // Adjust the iframe\n        var wrapperWidth = this.$el.innerWidth();\n        var wrapperHeight = this.$el.innerHeight();\n        var relativeRatio = (wrapperWidth / wrapperHeight) / (16 / 9);\n        var style = {};\n        if (relativeRatio >= 1.0) {\n            style['width'] = '100%';\n            style['height'] = (relativeRatio * 100) + '%';\n            style['inset-inline-start'] = '0';\n            style['inset-block-start'] = (-(relativeRatio - 1.0) / 2 * 100) + '%';\n        } else {\n            style['width'] = ((1 / relativeRatio) * 100) + '%';\n            style['height'] = '100%';\n            style['inset-inline-start'] = (-((1 / relativeRatio) - 1.0) / 2 * 100) + '%';\n            style['inset-block-start'] = '0';\n        }\n        this.$iframe.css(style);\n\n        void this.$iframe[0].offsetWidth; // Force style addition\n        this.$iframe.addClass('show');\n    },\n    /**\n     * Append background video related elements to the target.\n     *\n     * @private\n     */\n    _appendBgVideo: function () {\n        const allowedCookies = !this.el.dataset.needCookiesApproval;\n\n        var $oldContainer = this.$bgVideoContainer || this.$('> .o_bg_video_container');\n        this.$bgVideoContainer = $(renderToElement('website.background.video', {\n            videoSrc: allowedCookies ? this.videoSrc : \"about:blank\",\n            iframeID: this.iframeID,\n        }));\n        this.$iframe = this.$bgVideoContainer.find('.o_bg_video_iframe');\n        this.$iframe.one('load', () => {\n            this.$bgVideoContainer.find('.o_bg_video_loading').remove();\n            // When there is a \"slide in (left or right) animation\" element, we\n            // need to adjust the iframe size once it has been loaded, otherwise\n            // an horizontal scrollbar may appear.\n            this._adjustIframe();\n        });\n        this.$bgVideoContainer.prependTo(this.$el);\n        $oldContainer.remove();\n\n        if (!allowedCookies) {\n            // We don't add the optional cookies warning for background videos\n            // so that the fallback message doesn't appear behind the content.\n            this.__onEnableVideoPostCookiesAccepted = () => {\n                this.$iframe[0].src = this.videoSrc;\n            };\n            document.addEventListener(\n                \"optionalCookiesAccepted\",\n                this.__onEnableVideoPostCookiesAccepted,\n                { once: true }\n            );\n        }\n\n        this._adjustIframe();\n        this._triggerAutoplay(this.$iframe[0]);\n    },\n});\n\nregistry.anchorSlide = publicWidget.Widget.extend({\n    selector: 'a[href^=\"/\"][href*=\"#\"], a[href^=\"#\"]',\n    events: {\n        'click': '_onAnimateClick',\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {jQuery} $el the element to scroll to.\n     * @param {string} [scrollValue='true'] scroll value\n     * @returns {Promise}\n     */\n    async _scrollTo($el, scrollValue = 'true') {\n        return scrollTo($el[0], {\n            duration: scrollValue === \"true\" ? 500 : 0,\n            extraOffset: this._computeExtraOffset(),\n        });\n    },\n    /**\n     * @private\n     */\n    _computeExtraOffset() {\n        return 0;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onAnimateClick: function (ev) {\n        const ensureSlash = path => path.endsWith(\"/\") ? path : path + \"/\";\n        if (ensureSlash(this.el.pathname) !== ensureSlash(window.location.pathname)) {\n            return;\n        }\n        // Avoid flicker at destination in case of ending \"/\" difference.\n        if (this.el.pathname !== window.location.pathname) {\n            this.el.pathname = window.location.pathname;\n        }\n        var hash = this.el.hash;\n        if (!hash.length) {\n            return;\n        }\n        // Escape special characters to make the jQuery selector to work.\n        hash = '#' + $.escapeSelector(hash.substring(1));\n        var $anchor = $(hash);\n        const scrollValue = $anchor.attr('data-anchor');\n        if (!$anchor.length || !scrollValue) {\n            return;\n        }\n\n        const offcanvasEl = this.el.closest('.offcanvas.o_navbar_mobile');\n        if (offcanvasEl && offcanvasEl.classList.contains('show')) {\n            // Special case for anchors in offcanvas in mobile: we can't just\n            // _scrollTo() after preventDefault because preventDefault would\n            // prevent the offcanvas to be closed. The choice is then to close\n            // it ourselves manually and once it's fully closed, then start our\n            // own smooth scrolling.\n            ev.preventDefault();\n            Offcanvas.getInstance(offcanvasEl).hide();\n            offcanvasEl.addEventListener('hidden.bs.offcanvas',\n                () => {\n                    this._manageScroll(hash, $anchor, scrollValue);\n                },\n                // the listener must be automatically removed when invoked\n                { once: true }\n            );\n        } else {\n            ev.preventDefault();\n            this._manageScroll(hash, $anchor, scrollValue);\n        }\n    },\n    /**\n     *\n     * @param {string} hash\n     * @param {jQuery} $el the element to scroll to.\n     * @param {string} [scrollValue='true'] scroll value\n     * @private\n     */\n    _manageScroll(hash, $anchor, scrollValue = \"true\") {\n        if (hash === \"#top\" || hash === \"#bottom\") {\n            // If the anchor targets #top or #bottom, directly call the\n            // \"scrollTo\" function. The reason is that the header or the footer\n            // could have been removed from the DOM. By receiving a string as\n            // parameter, the \"scrollTo\" function handles the scroll to the top\n            // or to the bottom of the document even if the header or the\n            // footer is removed from the DOM.\n            scrollTo(hash, {\n                duration: 500,\n                extraOffset: this._computeExtraOffset(),\n            });\n        } else {\n            this._scrollTo($anchor, scrollValue);\n        }\n    },\n});\n\nregistry.FullScreenHeight = publicWidget.Widget.extend({\n    selector: '.o_full_screen_height',\n    disabledInEditableMode: false,\n\n    /**\n     * @override\n     */\n    start() {\n        this.inModal = !!this.el.closest('.modal');\n\n        // TODO maybe review the way the public widgets work for non-visible-at-\n        // load snippets -> probably better to not do anything for them and\n        // start the widgets only once they become visible..?\n        if (this.$el.is(':not(:visible)') || this.$el.outerHeight() > this._computeIdealHeight()) {\n            // Only initialize if taller than the ideal height as some extra css\n            // rules may alter the full-screen-height class behavior in some\n            // cases (blog...).\n            this._adaptSize();\n            $(window).on('resize.FullScreenHeight', debounce(() => this._adaptSize(), 250));\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        $(window).off('.FullScreenHeight');\n        this.el.style.setProperty('min-height', '');\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _adaptSize() {\n        const height = this._computeIdealHeight();\n        this.el.style.setProperty('min-height', `${height}px`, 'important');\n    },\n    /**\n     * @private\n     */\n    _computeIdealHeight() {\n        const windowHeight = $(window).outerHeight();\n        if (this.inModal) {\n            return windowHeight;\n        }\n\n        // Doing it that way allows to considerer fixed headers, hidden headers,\n        // connected users, ...\n        const firstContentEl = $('#wrapwrap > main > :first-child')[0]; // first child to consider the padding-top of main\n        const mainTopPos = firstContentEl.getBoundingClientRect().top + document.documentElement.scrollTop;\n        return (windowHeight - mainTopPos);\n    },\n});\n\nregistry.ScrollButton = registry.anchorSlide.extend({\n    selector: '.o_scroll_button',\n\n    /**\n     * @override\n     */\n    _onAnimateClick: function (ev) {\n        ev.preventDefault();\n        // Scroll to the next visible element after the current one.\n        const currentSectionEl = this.el.closest('section');\n        let nextEl = currentSectionEl.nextElementSibling;\n        while (nextEl) {\n            if ($(nextEl).is(':visible')) {\n                this._scrollTo($(nextEl));\n                return;\n            }\n            nextEl = nextEl.nextElementSibling;\n        }\n    },\n});\n\nregistry.FooterSlideout = publicWidget.Widget.extend({\n    selector: '#wrapwrap',\n    selectorHas: '.o_footer_slideout',\n    disabledInEditableMode: false,\n\n    /**\n     * @override\n     */\n    async start() {\n        const $main = this.$('> main');\n        const slideoutEffect = $main.outerHeight() >= $(window).outerHeight();\n        this.el.classList.toggle('o_footer_effect_enable', slideoutEffect);\n\n        // On safari, add a pixel div over the footer, after in the DOM, and add\n        // a background attachment on it as it fixes the glitches that appear\n        // when scrolling the page with a footer slide out.\n        // TODO check if the hack is still needed (might have been fixed when\n        // the scrollbar was restored to its natural position).\n        if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {\n            this.__pixelEl = document.createElement('div');\n            this.__pixelEl.style.width = `1px`;\n            this.__pixelEl.style.height = `1px`;\n            this.__pixelEl.style.marginTop = `-1px`;\n            this.__pixelEl.style.backgroundColor = \"transparent\";\n            this.__pixelEl.style.backgroundAttachment = \"fixed\";\n            this.__pixelEl.style.backgroundImage = \"url(/website/static/src/img/website_logo.svg)\";\n            this.el.appendChild(this.__pixelEl);\n        }\n\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        this.el.classList.remove('o_footer_effect_enable');\n        if (this.__pixelEl) {\n            this.__pixelEl.remove();\n        }\n    },\n});\n\nregistry.BottomFixedElement = publicWidget.Widget.extend({\n    selector: '#wrapwrap',\n\n    /**\n     * @override\n     */\n    async start() {\n        this.$scrollingElement = $().getScrollingElement();\n        this.$scrollingTarget = $().getScrollingTarget(this.$scrollingElement);\n        this.__hideBottomFixedElements = debounce(() => this._hideBottomFixedElements(), 100);\n        this.$scrollingTarget.on('scroll.bottom_fixed_element', this.__hideBottomFixedElements);\n        $(window).on('resize.bottom_fixed_element', this.__hideBottomFixedElements);\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        this.$scrollingTarget.off('.bottom_fixed_element');\n        $(window).off('.bottom_fixed_element');\n        this._restoreBottomFixedElements($('.o_bottom_fixed_element'));\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Hides the elements that are fixed at the bottom of the screen if the\n     * scroll reaches the bottom of the page and if the elements hide a button.\n     *\n     * @private\n     */\n    _hideBottomFixedElements() {\n        // Note: check in the whole DOM instead of #wrapwrap as unfortunately\n        // some things are still put outside of the #wrapwrap (like the livechat\n        // button which is the main reason of this code).\n        const $bottomFixedElements = $('.o_bottom_fixed_element');\n        if (!$bottomFixedElements.length) {\n            return;\n        }\n\n        // The bottom fixed elements are always hidden when a modal is open\n        // thanks to the CSS that is based on the 'modal-open' class added to\n        // the body. However, when the modal does not have a backdrop (e.g.\n        // cookies bar), this 'modal-open' class is not added. That's why we\n        // handle it here. Note that the popup widget code triggers a 'scroll'\n        // event when the modal is hidden to make the bottom fixed elements\n        // reappear.\n        if (this.el.querySelector('.s_popup_no_backdrop.show')) {\n            for (const el of $bottomFixedElements) {\n                el.classList.add('o_bottom_fixed_element_hidden');\n            }\n            return;\n        }\n\n        this._restoreBottomFixedElements($bottomFixedElements);\n        if ((this.$scrollingElement[0].offsetHeight + this.$scrollingElement[0].scrollTop) >= (this.$scrollingElement[0].scrollHeight - 2)) {\n            const buttonEls = [...this.$('a:visible, .btn:visible')];\n            for (const el of $bottomFixedElements) {\n                const elRect = el.getBoundingClientRect();\n                const hiddenButtonEl = touching(buttonEls, {\n                    top: elRect.top,\n                    right: elRect.right,\n                    bottom: elRect.bottom,\n                    left: elRect.left,\n                    width: elRect.width,\n                    height: elRect.height,\n                    x: elRect.x,\n                    y: elRect.y,\n                });\n                if (hiddenButtonEl) {\n                    if (el.classList.contains('o_bottom_fixed_element_move_up')) {\n                        el.style.marginBottom = window.innerHeight - hiddenButtonEl.getBoundingClientRect().top + 5 + 'px';\n                    } else {\n                        el.classList.add('o_bottom_fixed_element_hidden');\n                    }\n                }\n            }\n        }\n    },\n    /**\n     * @private\n     * @param {jQuery} $elements bottom fixed elements to restore.\n     */\n    _restoreBottomFixedElements($elements) {\n        $elements.removeClass('o_bottom_fixed_element_hidden');\n        $elements.filter('.o_bottom_fixed_element_move_up').css('margin-bottom', '');\n    },\n});\n\nregistry.WebsiteAnimate = publicWidget.Widget.extend({\n    selector: '#wrapwrap',\n    disabledInEditableMode: false,\n\n    offsetRatio: 0.3, // Dynamic offset ratio: 0.3 = (element's height/3)\n    offsetMin: 10, // Minimum offset for small elements (in pixels)\n\n    /**\n     * @override\n     */\n    start() {\n        this.lastScroll = 0;\n        this.$scrollingElement = $().getScrollingElement();\n        this.$scrollingTarget = $().getScrollingTarget(this.$scrollingElement);\n        this.$animatedElements = this.$('.o_animate');\n\n        // Fix for \"transform: none\" not overriding keyframe transforms on\n        // some iPhone using Safari. Note that all animated elements are checked\n        // (not only one) as the bug is not systematic and may depend on some\n        // other conditions (for example: an animated image in a block which is\n        // hidden on mobile would not have the issue).\n        const couldOverflowBecauseOfSafariBug = [...this.$animatedElements].some(el => {\n            return window.getComputedStyle(el).transform !== 'none';\n        });\n        this.forceOverflowXYHidden = false;\n        if (couldOverflowBecauseOfSafariBug) {\n            this._toggleOverflowXYHidden(true);\n            // Now prevent any call to _toggleOverflowXYHidden to have an effect\n            this.forceOverflowXYHidden = true;\n        }\n\n        // By default, elements are hidden by the css of o_animate.\n        // Render elements and trigger the animation then pause it in state 0.\n        this.$animatedElements.toArray().forEach((el) => {\n            if (el.closest('.dropdown')) {\n                el.classList.add('o_animate_in_dropdown');\n                return;\n            }\n            if (!el.classList.contains('o_animate_on_scroll')) {\n                this._resetAnimation($(el));\n            }\n        });\n        // Then we render all the elements, the ones which are invisible\n        // in state 0 (like fade_in for example) will stay invisible.\n        this.$animatedElements.css(\"visibility\", \"visible\");\n\n        // We use addEventListener instead of jQuery because we need 'capture'.\n        // Setting capture to true allows to take advantage of event bubbling\n        // for events that otherwise don\u2019t support it. (e.g. useful when\n        // scrolling a modal)\n        this.__onScrollWebsiteAnimate = throttleForAnimation(this._onScrollWebsiteAnimate.bind(this));\n        this.$scrollingTarget[0].addEventListener('scroll', this.__onScrollWebsiteAnimate, {capture: true});\n\n        $(window).on('resize.o_animate, shown.bs.modal.o_animate, slid.bs.carousel.o_animate, shown.bs.tab.o_animate, shown.bs.collapse.o_animate', () => {\n            this.windowsHeight = $(window).height();\n            this._scrollWebsiteAnimate(this.$scrollingElement[0]);\n        }).trigger(\"resize\");\n\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        this.$('.o_animate')\n            .removeClass('o_animating o_animated o_animate_preview o_animate_in_dropdown')\n            .css({\n                'animation-name': '',\n                'animation-play-state': '',\n                'visibility': '',\n            });\n        $(window).off('.o_animate');\n        this.__onScrollWebsiteAnimate.cancel();\n        this.$scrollingTarget[0].removeEventListener('scroll', this.__onScrollWebsiteAnimate, {capture: true});\n        this.$scrollingElement[0].classList.remove('o_wanim_overflow_xy_hidden');\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Starts animation and/or update element's state.\n     *\n     * @private\n     * @param {jQuery} $el\n     */\n    _startAnimation($el) {\n        // Forces the browser to redraw using setTimeout.\n        setTimeout(() => {\n            this._toggleOverflowXYHidden(true);\n            $el\n            .css({\"animation-play-state\": \"running\"})\n            .addClass(\"o_animating\")\n            .one('webkitAnimationEnd oanimationend msAnimationEnd animationend', () => {\n                $el.addClass(\"o_animated\").removeClass(\"o_animating\");\n                this._toggleOverflowXYHidden(false);\n                $(window).trigger(\"resize\");\n            });\n        });\n    },\n    /**\n     * @private\n     * @param {jQuery} $el\n     */\n    _resetAnimation($el) {\n        const animationName = $el.css(\"animation-name\");\n        $el.css({\"animation-name\": \"dummy-none\", \"animation-play-state\": \"\"})\n           .removeClass(\"o_animated o_animating\");\n\n        this._toggleOverflowXYHidden(false);\n        // trigger a DOM reflow\n        void $el[0].offsetWidth;\n        $el.css({'animation-name': animationName , 'animation-play-state': 'paused'});\n    },\n    /**\n     * Shows/hides the horizontal scrollbar and prevents flicker of the page\n     * height (on the slideout footer).\n     *\n     * @private\n     * @param {Boolean} add\n     */\n    _toggleOverflowXYHidden(add) {\n        if (this.forceOverflowXYHidden) {\n            return;\n        }\n        if (add) {\n            this.$scrollingElement[0].classList.add('o_wanim_overflow_xy_hidden');\n        } else if (!this.$scrollingElement.find('.o_animating').length) {\n            this.$scrollingElement[0].classList.remove('o_wanim_overflow_xy_hidden');\n        }\n    },\n    /**\n     * Gets element top offset by not taking CSS transforms into calculations.\n     *\n     * @private\n     * @param {Element} el\n     * @param {HTMLElement} [topEl] if specified, calculates the top distance to\n     *     this element.\n     */\n    _getElementOffsetTop(el, topEl) {\n        // Loop through the DOM tree and add its parent's offset to get page offset.\n        var top = 0;\n        do {\n            top += el.offsetTop || 0;\n            el = el.offsetParent;\n            if (topEl && el === topEl) {\n                return top;\n            }\n        } while (el);\n        return top;\n    },\n    /**\n     * @private\n     * @param {Element} el\n     */\n    _scrollWebsiteAnimate(el) {\n        this.$('.o_animate:not(.o_animate_in_dropdown)').toArray().forEach((el) => {\n            const $el = $(el);\n            const elHeight = el.offsetHeight;\n            const animateOnScroll = el.classList.contains('o_animate_on_scroll');\n            let elOffset = animateOnScroll ? 0 : Math.max((elHeight * this.offsetRatio), this.offsetMin);\n            const state = $el.css(\"animation-play-state\");\n\n            // We need to offset for the change in position from some animation.\n            // So we get the top value by not taking CSS transforms into calculations.\n            // Cookies bar might be opened and considered as a modal but it is\n            // not really one when there is no backdrop (eg 'discrete' layout),\n            // and should not be used as scrollTop value.\n            const closestModal = $el.closest(\".modal:visible\")[0];\n            let scrollTop = this.$scrollingElement[0].scrollTop;\n            if (closestModal) {\n                scrollTop = closestModal.classList.contains(\"s_popup_no_backdrop\") ?\n                    closestModal.querySelector(\".modal-content\").scrollTop :\n                    closestModal.scrollTop;\n            }\n            const elTop = this._getElementOffsetTop(el) - scrollTop;\n            let visible;\n            const footerEl = el.closest('.o_footer_slideout');\n            const wrapEl = this.el;\n            if (footerEl && wrapEl.classList.contains('o_footer_effect_enable')) {\n                // Since the footer slideout is always in the viewport but not\n                // always displayed, the way to calculate if an element is\n                // visible in the footer is different. We decided to handle this\n                // case specifically instead of a generic solution using\n                // elementFromPoint as it is a rare case and the implementation\n                // would have been too complicated for such a small use case.\n                const actualScroll = wrapEl.scrollTop + this.windowsHeight;\n                const totalScrollHeight = wrapEl.scrollHeight;\n                const heightFromFooter = this._getElementOffsetTop(el, footerEl);\n                visible = actualScroll >=\n                    totalScrollHeight - heightFromFooter - elHeight + elOffset;\n            } else {\n                visible = this.windowsHeight > (elTop + elOffset) &&\n                    0 < (elTop + elHeight - elOffset);\n            }\n            if (animateOnScroll) {\n                if (visible) {\n                    const start = 100 / (parseFloat(el.dataset.scrollZoneStart) || 1);\n                    const end = 100 / (parseFloat(el.dataset.scrollZoneEnd) || 1);\n                    const out = el.classList.contains('o_animate_out');\n                    const ratio = (out ? elTop + elHeight : elTop) / (this.windowsHeight - (this.windowsHeight / start));\n                    const duration = parseFloat(window.getComputedStyle(el).animationDuration);\n                    const delay = (ratio - 1) * (duration * end);\n                    el.style.animationDelay = (out ? - duration - delay : delay) + \"s\";\n                    el.classList.add('o_animating');\n                    this._toggleOverflowXYHidden(true);\n                } else if (el.classList.contains('o_animating')) {\n                    el.classList.remove('o_animating');\n                    this._toggleOverflowXYHidden(false);\n                }\n            } else {\n                if (visible && state === 'paused') {\n                    $el.addClass('o_visible');\n                    this._startAnimation($el);\n                } else if (!visible && $el.hasClass('o_animate_both_scroll') && state === 'running') {\n                    $el.removeClass('o_visible');\n                    this._resetAnimation($el);\n                }\n            }\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onScrollWebsiteAnimate() {\n        // Note: Do not rely on ev.currentTarget which might be lost by Chrome.\n        this._scrollWebsiteAnimate(this.$scrollingElement[0]);\n    },\n});\n\n/**\n * The websites, by default, use image lazy loading via the loading=\"lazy\"\n * attribute on <img> elements. However, this does not work great on all\n * browsers. This widget fixes the behaviors with as less code as possible.\n */\nregistry.ImagesLazyLoading = publicWidget.Widget.extend({\n    selector: '#wrapwrap',\n\n    /**\n     * @override\n     */\n    start() {\n        // For each image on the page, force a 1px min-height so that Chrome\n        // understands the image exists on different zoom sizes of the browser.\n        // Indeed, without this, on a 90% zoom, some images were never loaded.\n        // Once the image has been loaded, the 1px min-height is removed.\n        // Note: another possible solution without JS would be this CSS rule:\n        // ```\n        // [loading=\"lazy\"] {\n        //     min-height: 1px;\n        // }\n        // ```\n        // This would solve the problem the same way with a CSS rule with a\n        // very small priority (any class setting a min-height would still have\n        // priority). However, the min-height would always be forced even once\n        // the image is loaded, which could mess with some layouts relying on\n        // the image intrinsic min-height.\n        const imgEls = this.el.querySelectorAll('img[loading=\"lazy\"]');\n        for (const imgEl of imgEls) {\n            this._updateImgMinHeight(imgEl);\n            wUtils.onceAllImagesLoaded($(imgEl)).then(() => {\n                if (this.isDestroyed()) {\n                    return;\n                }\n                this._restoreImage(imgEl);\n            });\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        const imgEls = this.el.querySelectorAll('img[data-lazy-loading-initial-min-height]');\n        for (const imgEl of imgEls) {\n            this._restoreImage(imgEl);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {HTMLImageElement} imgEl\n     */\n    _restoreImage(imgEl) {\n        this._updateImgMinHeight(imgEl, true);\n    },\n    /**\n     * Updates the image element style with the corresponding min-height.\n     * If the editor is enabled, it deactivates the observer during the CSS\n     * update.\n     *\n     * @param {HTMLElement} imgEl - The image element to update the minimum\n     *        height of.\n     * @param {boolean} [reset=false] - Whether to remove the minimum height\n     *        and restore the initial value.\n     */\n    _updateImgMinHeight(imgEl, reset = false) {\n        if (this.options.wysiwyg) {\n            this.options.wysiwyg.odooEditor.observerUnactive('_updateImgMinHeight');\n        }\n        if (reset) {\n            imgEl.style.minHeight = imgEl.dataset.lazyLoadingInitialMinHeight;\n            delete imgEl.dataset.lazyLoadingInitialMinHeight;\n        } else {\n            // Write initial min-height on the dataset, so that it can also\n            // be properly restored on widget destroy.\n            imgEl.dataset.lazyLoadingInitialMinHeight = imgEl.style.minHeight;\n            imgEl.style.minHeight = '1px';\n        }\n        if (this.options.wysiwyg) {\n            this.options.wysiwyg.odooEditor.observerActive('_updateImgMinHeight');\n        }\n    },\n});\n\n/**\n * @todo while this solution mitigates the issue, it is not fixing it entirely\n * but mainly, we should find a better solution than a JS solution as soon as\n * one is available and ideally without having to make ugly patches to the SVGs.\n *\n * Due to a bug on Chrome when using browser zoom, there is sometimes a gap\n * between sections with shapes. This gap is due to a rounding issue when\n * positioning the SVG background images. This code reduces the rounding error\n * by ensuring that shape elements always have a width value as close to an\n * integer as possible.\n *\n * Note: a gap also appears between some shapes without zoom. This is likely\n * due to error in the shapes themselves. Many things were done to try and fix\n * this, but the remaining errors will likely be fixed with a review of the\n * shapes in future Odoo versions.\n *\n * /!\\\n * If a better solution for stable comes up, this widget behavior may be\n * disabled, avoid depending on it if possible.\n * /!\\\n */\nregistry.ZoomedBackgroundShape = publicWidget.Widget.extend({\n    selector: '.o_we_shape',\n    disabledInEditableMode: false,\n\n    /**\n     * @override\n     */\n    start() {\n        this._onBackgroundShapeResize();\n        this.throttledShapeResize = throttleForAnimation(() => this._onBackgroundShapeResize());\n        window.addEventListener('resize', this.throttledShapeResize);\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._updateShapePosition();\n        this.throttledShapeResize.cancel();\n        window.removeEventListener('resize', this.throttledShapeResize);\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Updates the left and right offset of the shape.\n     *\n     * @private\n     * @param {string} offset\n     */\n    _updateShapePosition(offset = '') {\n        this.el.style.left = offset;\n        this.el.style.right = offset;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onBackgroundShapeResize() {\n        this._updateShapePosition();\n        // Get the decimal part of the shape element width.\n        let decimalPart = this.el.getBoundingClientRect().width % 1;\n        // Round to two decimal places.\n        decimalPart = Math.round((decimalPart + Number.EPSILON) * 100) / 100;\n        // If there is a decimal part. (e.g. Chrome + browser zoom enabled)\n        if (decimalPart > 0) {\n            // Compensate for the gap by giving an integer width value to the\n            // shape by changing its \"right\" and \"left\" positions.\n            let offset = (decimalPart < 0.5 ? decimalPart : decimalPart - 1) / 2;\n            // This never causes the horizontal scrollbar to appear because it\n            // only appears if the overflow to the right exceeds 0.333px.\n            this._updateShapePosition(offset + 'px');\n        }\n    },\n});\n\nregistry.ImageShapeHoverEffet = publicWidget.Widget.extend({\n    selector: \"img[data-hover-effect]\",\n    disabledInEditableMode: false,\n    events: {\n        \"mouseenter\": \"_onMouseEnter\",\n        \"mouseleave\": \"_onMouseLeave\",\n    },\n\n    /**\n     * @constructor\n     */\n    init() {\n        this._super(...arguments);\n        this.lastMouseEvent = Promise.resolve();\n    },\n    /**\n     * @override\n     */\n    start() {\n        this._super(...arguments);\n        this.originalImgSrc = this.el.getAttribute('src');\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        if (this.el.dataset.originalSrcBeforeHover && !this.el.classList.contains(\"o_modified_image_to_save\")) {\n            // Replace the image source by its original one if it has not been\n            // modified in edit mode.\n            this.el.src = this.el.dataset.originalSrcBeforeHover;\n        } else if (this.originalImgSrc && (this.lastImgSrc === this.el.getAttribute(\"src\"))) {\n            this.el.src = this.originalImgSrc;\n        }\n        delete this.el.dataset.originalSrcBeforeHover;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onMouseEnter() {\n        if (!this.originalImgSrc || !this.$target[0].dataset.hoverEffect) {\n            return;\n        }\n        this.lastMouseEvent = this.lastMouseEvent.then(() => new Promise((resolve) => {\n            if (!this.svgInEl) {\n                fetch(this.el.src)\n                    .then(response => response.text())\n                    .then(text => {\n                        const parser = new DOMParser();\n                        const result = parser.parseFromString(text, \"text/xml\");\n                        const svg = result.getElementsByTagName(\"svg\")[0];\n                        this.svgInEl = svg;\n                        if (!this.svgInEl) {\n                            resolve();\n                            return;\n                        }\n                        // Start animations.\n                        const animateEls = this.svgInEl.querySelectorAll(\"#hoverEffects animateTransform, #hoverEffects animate\");\n                        animateEls.forEach(animateTransformEl => {\n                            animateTransformEl.removeAttribute(\"begin\");\n                        });\n                        this._setImgSrc(this.svgInEl, resolve);\n                    }).catch(() => {\n                        // Could be the case if somehow the `src` is an absolute\n                        // URL from another domain.\n                    });\n            } else {\n                this._setImgSrc(this.svgInEl, resolve);\n            }\n        }));\n    },\n    /**\n     * @private\n     */\n    _onMouseLeave() {\n        this.lastMouseEvent = this.lastMouseEvent.then(() => new Promise((resolve) => {\n            if (!this.originalImgSrc || !this.svgInEl || !this.$target[0].dataset.hoverEffect) {\n                resolve();\n                return;\n            }\n            if (!this.svgOutEl) {\n                // Reverse animations.\n                this.svgOutEl = this.svgInEl.cloneNode(true);\n                const animateTransformEls = this.svgOutEl.querySelectorAll(\"#hoverEffects animateTransform, #hoverEffects animate\");\n                animateTransformEls.forEach(animateTransformEl => {\n                    let valuesValue = animateTransformEl.getAttribute(\"values\");\n                    valuesValue = valuesValue.split(\";\").reverse().join(\";\");\n                    animateTransformEl.setAttribute(\"values\", valuesValue);\n                });\n            }\n            this._setImgSrc(this.svgOutEl, resolve);\n        }));\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Converts the SVG to a data URI and set it as the image source.\n     *\n\ufffc    * @private\n     * @param {HTMLElement} svg\n\ufffc    */\n    _setImgSrc(svg, resolve) {\n        // Add random class to prevent browser from caching image. Otherwise the\n        // animations do not trigger more than once.\n        const previousRandomClass = [...svg.classList].find(cl => cl.startsWith(\"o_shape_anim_random_\"));\n        svg.classList.remove(previousRandomClass);\n        svg.classList.add(\"o_shape_anim_random_\" + Date.now());\n        // Convert the SVG element to a data URI.\n        const svg64 = btoa(new XMLSerializer().serializeToString(svg));\n        // The image is preloaded to avoid a flickering when it is added to the\n        // DOM.\n        const preloadedImg = new Image();\n        preloadedImg.src = `data:image/svg+xml;base64,${svg64}`;\n        preloadedImg.onload = () => {\n            if (this.isDestroyed()) {\n                // In some cases, it is possible for the \"preloadedImg\" to\n                // finish loading while the widget has already been destroyed.\n                // So, we do not set the image source because that can cause\n                // unexpected reverse of the animation.\n                resolve();\n                return;\n            }\n            this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerUnactive(\"setImgHoverEffectSrc\");\n            if (this.editableMode && !this.el.dataset.originalSrcBeforeHover) {\n                this.el.dataset.originalSrcBeforeHover = this.originalImgSrc;\n            }\n            this.el.src = preloadedImg.getAttribute('src');\n            this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerActive(\"setImgHoverEffectSrc\");\n            this.lastImgSrc = preloadedImg.getAttribute('src');\n            this.el.onload = () => {\n                resolve();\n            };\n        };\n    },\n});\n\nregistry.TextHighlight = publicWidget.Widget.extend({\n    selector: '#wrapwrap',\n    disabledInEditableMode: false,\n\n    /**\n     * @override\n     */\n    async start() {\n        // We need to adapt the text highlights on resize (E.g. custom fonts\n        // loading, layout option changes, window resized...), mainly to take in\n        // consideration the rendered line breaks in text nodes... But after\n        // every adjustment, the `ResizeObserver` will unfortunately immediately\n        // notify a size change once new highlight items are observed leading to\n        // an infinite loop. To avoid that, we use a lock map (`observerLock`)\n        // to block the callback on this first notification for observed items.\n        this.observerLock = new Map();\n        this.resizeObserver = new window.ResizeObserver(entries => {\n            // Some options, like the popup, trigger a resize after a delay\n            // before the page is saved. This causes the highlights to be added\n            // back to the DOM after the \"TextHighlight\" widget has been\n            // destroyed. This is why the following line is needed.\n            if (this.isDestroyed()) {\n                return;\n            }\n            window.requestAnimationFrame(() => {\n                const textHighlightEls = new Set();\n                entries.forEach(entry => {\n                    const target = entry.target;\n                    if (this.observerLock.get(target)) {\n                        // Unlock the target, the next resize will trigger a\n                        // highlight adaptation.\n                        return this.observerLock.set(target, false);\n                    }\n                    const topTextEl = target.closest(\".o_text_highlight\");\n                    for (const el of topTextEl\n                        ? [topTextEl]\n                        : target.querySelectorAll(\":scope .o_text_highlight\")) {\n                        textHighlightEls.add(el);\n                    }\n                });\n                textHighlightEls.forEach(textHighlightEl => {\n                    for (const textHighlightItemEl of this._getHighlightItems(textHighlightEl)) {\n                        // Unobserve the highlight lines (they will be replaced\n                        // by new ones after the update).\n                        this.resizeObserver.unobserve(textHighlightItemEl);\n                    }\n                    // Adapt the highlight (new items are automatically locked\n                    // and observed).\n                    switchTextHighlight(textHighlightEl);\n                });\n            });\n        });\n\n        this.el.addEventListener(\"text_highlight_added\", this._onTextHighlightAdded.bind(this));\n        this.el.addEventListener(\"text_highlight_remove\", this._onTextHighlightRemove.bind(this));\n        // Text highlights are saved with a single wrapper that contains all\n        // information to build the effects, So we need to make the adaptation\n        // here to show the SVGs.\n        for (const textEl of this.el.querySelectorAll(\".o_text_highlight\")) {\n            applyTextHighlight(textEl);\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        // We only save the highlight information on the main text wrapper,\n        // the full structure will be restored on page load.\n        for (const textHighlightEl of this.el.querySelectorAll(\".o_text_highlight\")) {\n            removeTextHighlight(textHighlightEl);\n        }\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * The `resizeObserver` ignores an element if it has an inline display.\n     * We need to target the closest non-inline parent.\n     *\n     * @private\n     * @param {HTMLElement} el\n     */\n    _closestToObserve(el) {\n        if (el === this.el || !el) {\n            return null;\n        }\n        if (window.getComputedStyle(el).display !== \"inline\") {\n            return el;\n        }\n        return this._closestToObserve(el.parentElement);\n    },\n    /**\n     * Returns a list of text highlight items (lines) in the provided element.\n     *\n     * @private\n     * @param {HTMLElement} el\n     */\n    _getHighlightItems(el = this.el) {\n        return el.querySelectorAll(\":scope .o_text_highlight_item\");\n    },\n    /**\n     * Returns a list of highlight elements to observe.\n     *\n     * @private\n     * @param {HTMLElement} topTextEl\n     */\n    _getObservedEls(topTextEl) {\n        const closestToObserve = this._closestToObserve(topTextEl);\n        return [\n            ...(closestToObserve ? [closestToObserve] : []),\n            ...this._getHighlightItems(topTextEl),\n        ];\n    },\n    /**\n     * @private\n     * @param {HTMLElement} topTextEl the element where the \"resize\" should\n     * be observed.\n     */\n    _observeHighlightResize(topTextEl) {\n        // The `ResizeObserver` cannot detect the width change on highlight\n        // units (`.o_text_highlight_item`) as long as the width of the entire\n        // `.o_text_highlight` element remains the same, so we need to observe\n        // each one of them and do the adjustment only once for the whole text.\n        for (const highlightItemEl of this._getObservedEls(topTextEl)) {\n            this.resizeObserver.observe(highlightItemEl);\n        }\n    },\n    /**\n     * Used to prevent the first callback triggered by `ResizeObserver` on new\n     * observed items.\n     *\n     * @private\n     * @param {HTMLElement} topTextEl the container of observed items.\n     */\n    _lockHighlightObserver(topTextEl) {\n        for (const targetEl of this._getObservedEls(topTextEl)) {\n            this.observerLock.set(targetEl, true);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onTextHighlightAdded({ target }) {\n        this._lockHighlightObserver(target);\n        this._observeHighlightResize(target);\n    },\n    /**\n     * @private\n     */\n    _onTextHighlightRemove({ target }) {\n        // We don't need to track the removed text highlight items after\n        // highlight adaptations.\n        for (const highlightItemEl of this._getHighlightItems(target)) {\n            this.observerLock.delete(highlightItemEl);\n        }\n    },\n});\n\nexport default {\n    Widget: publicWidget.Widget,\n    Animation: Animation,\n    registry: registry,\n\n    Class: Animation, // Deprecated\n};\n", "/** @odoo-module **/\n\n//\n// This file is meant to allow to switch the type of an input #password\n// from password to text on mousedown on an input group.\n// On mouse down, we see the password in clear text\n// On mouse up, we hide it again.\n//\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\npublicWidget.registry.ShowPassword = publicWidget.Widget.extend({\n    selector: '#showPass',\n    events: {\n        'mousedown': '_onShowText',\n        'touchstart': '_onShowText',\n    },\n\n    /**\n     * @override\n     */\n    destroy: function () {\n        this._super(...arguments);\n        $('body').off(\".ShowPassword\");\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onShowPassword: function () {\n        this.$el.closest('.input-group').find('#password').attr('type', 'password');\n    },\n    /**\n     * @private\n     */\n    _onShowText: function () {\n        $('body').one('mouseup.ShowPassword touchend.ShowPassword', this._onShowPassword.bind(this));\n        this.$el.closest('.input-group').find('#password').attr('type', 'text');\n    },\n});\n\nexport default publicWidget.registry.ShowPassword;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport wUtils from \"@website/js/utils\";\n\npublicWidget.registry.postLink = publicWidget.Widget.extend({\n    selector: '.post_link',\n    events: {\n        'click': '_onClickPost',\n    },\n\n    /**\n     * @override\n     */\n    start() {\n        // Allows the link to be interacted with only when Javascript is loaded.\n        this.el.classList.add('o_post_link_js_loaded');\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        this.el.classList.remove('o_post_link_js_loaded');\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    _onClickPost: function (ev) {\n        ev.preventDefault();\n        const url = this.el.dataset.post || this.el.href;\n        let data = {};\n        for (let [key, value] of Object.entries(this.el.dataset)) {\n            if (key.startsWith('post_')) {\n                data[key.slice(5)] = value;\n            }\n        }\n        wUtils.sendRequest(url, data);\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\n\npublicWidget.registry.o_plausible_push = publicWidget.Widget.extend({\n    selector: '.js_plausible_push',\n\n    /**\n     * @override\n     */\n    start() {\n        window.plausible = window.plausible || function () {\n            (window.plausible.q = window.plausible.q || []).push(arguments);\n        };\n        this._push();\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Pushes the event `data-event-name` to Plausible with params\n     * `data-event-params`\n     */\n    _push() {\n        const evName = this.$el.data('event-name').toString();\n        const evParams = this.$el.data('event-params') || {};\n        window.plausible(evName, {props: evParams});\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { rpc } from \"@web/core/network/rpc\";\n\npublicWidget.registry.WebsiteControllerPageListingLayout = publicWidget.Widget.extend({\n    selector: \".o_website_listing_layout\",\n    disabledInEditableMode: true,\n    events: {\n        \"change .listing_layout_switcher input\": \"_onApplyLayoutChange\",\n    },\n    \n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onApplyLayoutChange(ev) {\n        const wysiwyg = this.options.wysiwyg;\n        if (wysiwyg) {\n            wysiwyg.odooEditor.observerUnactive(\"_onApplyLayoutChange\");\n        }\n        const clickedValue = ev.target.value;\n        const isList = clickedValue === \"list\";\n        if (!this.editableMode) {\n            rpc(\"/website/save_session_layout_mode\", {\n                layout_mode: isList ? \"list\" : \"grid\",\n                view_id: document\n                    .querySelector(\".listing_layout_switcher\")\n                    .getAttribute(\"data-view-id\"),\n            });\n        }\n\n        const activeClasses = ev.target.parentElement.dataset.activeClasses.split(\" \");\n        ev.target.parentElement.querySelectorAll(\".btn\").forEach((btn) => {\n            activeClasses.map((c) => btn.classList.toggle(c));\n        });\n\n        const el = document.querySelector(isList ? \".o_website_grid\" : \".o_website_list\");\n        this._toggle_view_mode(el, isList);\n\n        if (wysiwyg) {\n            wysiwyg.odooEditor.observerActive(\"_onApplyLayoutChange\");\n        }\n    },\n\n    _toggle_view_mode(el, isList) {\n        if (el) {\n            el.classList.toggle(\"o_website_list\", isList);\n            el.classList.toggle(\"o_website_grid\", !isList);\n            const classList = isList ? \"\" : \"col-lg-3 col-md-4 col-sm-6 px-2 col-xs-12\";\n            // each card must have the correct bootstrap classes\n            [...document.querySelectorAll(\".o_website_list > div, .o_website_grid > div\")].forEach((card) => {\n                card.classList = classList;\n            });\n        }\n    }\n});\n", "/* @odoo-module */\n//\n// This file is meant to regroup your javascript code. You can either copy/past\n// any code that should be executed on each page loading or write your own\n// taking advantage of the Odoo framework to create new behaviors or modify\n// existing ones. For example, doing this will greet any visitor with a 'Hello,\n// world !' message in a popup:\n//\n/*\nimport { ConfirmationDialog } from '@web/core/confirmation_dialog/confirmation_dialog';\nimport publicWidget from '@web/legacy/js/public/public_widget';\n\npublicWidget.registry.HelloWorldPopup = publicWidget.Widget.extend({\n    selector: '#wrapwrap',\n\n    init() {\n        this.dialog = this.bindService(\"dialog\");\n    },\n    start() {\n        this.dialog.add(ConfirmationDialog, { body: 'Hello World' });\n        return this._super.apply(this, arguments);\n    },\n});\n*/\n", "/** @odoo-module **/\n\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(cookie, {\n    isAllowedCookie(type) {\n        if (type === \"optional\") {\n            if (!document.getElementById(\"cookies-consent-essential\")) {\n                // Cookies bar is disabled on this website.\n                return true;\n            }\n            const consents = JSON.parse(cookie.get(\"website_cookies_bar\") || \"{}\");\n\n            // pre-16.0 compatibility, `website_cookies_bar` was `\"true\"`.\n            // In that case we delete that cookie and let the user choose again.\n            if (typeof consents !== \"object\") {\n                cookie.delete(\"website_cookies_bar\");\n                return false;\n            }\n\n            if (\"optional\" in consents) {\n                return consents[\"optional\"];\n            }\n            return false;\n        }\n        return true;\n    },\n    set(key, value, ttl, type = \"required\") {\n        super.set(key, value, this.isAllowedCookie(type) ? ttl : 0);\n    },\n});\n", "/** @odoo-module **/\n\nimport * as OdooEditorLib from \"@web_editor/js/editor/odoo-editor/src/utils/utils\";\n\n// SVG generator: contains all information needed to draw highlight SVGs\n// according to text dimensions, highlight style,...\nconst _textHighlightFactory = {\n    underline: targetEl => {\n        return drawPath(targetEl, {mode: \"line\"});\n    },\n    freehand_1: targetEl => {\n        const template = (w, h) => [`M 0,${h * 1.1} C ${w / 8},${h * 1.05} ${w / 4},${h} ${w},${h}`];\n        return drawPath(targetEl, {mode: \"free\", template});\n    },\n    freehand_2: targetEl => {\n        const template = (w, h) => [`M181.27 13.873c-.451-1.976-.993-3.421-1.072-4.9-.125-2.214-.61-4.856.384-6.539.756-1.287 3.636-2.055 5.443-1.852 3.455.395 7.001 1.231` +\n        ` 10.14 2.676 1.728.802 3.174 3.06 3.817 4.98.237.712-1.953 2.824-3.399 3.4-2.766 1.095-5.748 1.75-8.706 2.179-2.394.339-4.879.068-6.584.068l-.023-.012ZM8.416 3.90` +\n        `2c3.862.26 7.78.249 11.574.926 1.65.294 3.027 2.033 4.54 3.117-1.095 1.186-1.987 2.982-3.343 3.456a67.118 67.118 0 0 1-11.19 2.823c-3.253.53-6.494-.339-8.617-2.98` +\n        `1C.364 9.978-.302 7.686.138 6.263c.361-1.152 2.54-2 4.077-2.44 1.287-.372 2.789.046 4.2.102v-.023Zm154.267 9.983c-4.291-.305-8.153-1.58-9.915-5.623-.745-1.694-.39` +\n        `5-4.382.474-6.121 1.073-2.168 3.512-1.965 5.613-1.005 2.541 1.174 5.251 2.157 7.509 3.76 1.502 1.073 3.557 3.445 3.207 4.574-.519 1.694-2.857 2.913-4.562 4.133-.5` +\n        `76.406-1.592.203-2.326.282ZM72.58 17.42c-2.733-1.807-5.307-3.004-7.137-4.913-.892-.925-.892-3.376-.361-4.776.407-1.05 2.304-2.112 3.546-2.135 3.602-.056 7.238.215` +\n        ` 10.818.723 3.828.542 5.15 4.1 2.213 6.539-2.439 2.021-5.77 2.958-9.079 4.562Zm30.795-.802c-2.507-1.536-5.228-2.823-7.397-4.743-.925-.813-1.377-3.297-.813-4.359.6` +\n        `78-1.265 2.677-2.507 4.11-2.518 3.016-.023 6.155.418 9.001 1.389 1.412.485 3.173 2.552 3.185 3.907 0 1.57-1.423 3.557-2.801 4.619-1.152.892-3.139.711-4.743 1.005-` +\n        `.181.226-.35.463-.531.689l-.011.01Zm-59.704-1.457c-2.066-1.163-4.788-2.224-6.82-4.054-.915-.824-1.04-3.478-.407-4.765.486-.983 2.722-1.559 4.156-1.502 2.676.101 5` +\n        `.398.542 7.95 1.332 1.457.452 3.523 1.75 3.681 2.891.18 1.31-1.13 3.309-2.383 4.201-1.411 1.005-3.466 1.118-6.188 1.886l.011.011Zm88.489-1.863c-2.643-1.48-5.567-2` +\n        `.62-7.803-4.574-1.005-.88-1.31-3.692-.667-5.002.509-1.04 2.982-1.615 4.529-1.513 2.032.135 4.054 1.027 6.007 1.772 2.485.95 5.026 2.236 4.382 5.455-.644 3.15-3.49` +\n        ` 2.947-5.963 3.004-.169.293-.327.575-.496.87l.011-.012Z`];\n        return drawPath(targetEl, {mode: \"fill\", template, SVGWidth: 200, SVGHeight: 18, position: \"bottom\"});\n    },\n    freehand_3: targetEl => {\n        const template = (w, h) => [`M189.705 18.285c-3.99.994-7.968 2.015-11.958 2.972-1.415.344-2.926 1.008-4.278.727-6.305-1.327-12.568-3.036-18.874-4.376-1.995-.42-4.2` +\n        `46-.701-6.133-.038-5.867 2.067-11.54 2.386-17.374-.242-1.491-.676-3.56-.421-5.125.217-5.523 2.22-10.789 3.597-16.494.127-1.64-.995-4.675-.038-6.584 1.148-6.102 3.` +\n        `789-12.01 4.414-18.198.434-.998-.638-2.681-.638-3.754-.115-6.852 3.355-13.404 2.858-20.043-1.008-1.5-.867-4.02-.6-5.608.307-7.528 4.35-14.842 5.702-22.07-.638-2.1` +\n        `44-1.875-3.71-.37-5.394 1.046-4.622 3.89-9.565 6.327-15.367 4.286C6.338 20.989.505 13.067.022 5.949-.085 4.38.194 1.753.955 1.332 2.253.617 4.537.553 5.588 1.51 7` +\n        `.55 3.27 9.18 5.77 10.52 8.296c2.82 5.269 4.15 5.766 8.504 2.156 1.555-1.288 2.992-2.768 4.396-4.286 4.022-4.311 7.143-4.465 11.26-.472 7.068 6.837 8.226 7.067 15` +\n        `.979 1.314 3.721-2.755 7.206-2.653 10.627.128 4.987 4.056 9.791 4.49 14.853.191 2.702-2.296 5.78-2.296 8.45.115 4.29 3.89 8.45 3.33 12.719.166.847-.638 1.705-1.26` +\n        `3 2.552-1.914 3.035-2.309 6.048-2.5 9.019.166 3.453 3.087 7.12 3.15 10.616.472 4.107-3.138 7.85-3.342 12.16-.306 3.668 2.59 7.83 1.964 11.594-.255 3.935-2.322 7.6` +\n        `67-2.488 11.409.408.365.28.794.612 1.213.65 6.799.549 13.522 3.394 20.428.779 1.887-.715 3.914-1.034 5.899-1.148 3.313-.192 6.659-.358 9.941 0 1.993.23 4.354.905 ` +\n        `5.737 2.436 1.308 1.429 2.113 4.235 2.123 6.442.022 3.023-2.424 3.431-4.472 3.597-1.887.153-3.796.038-5.695.038-.053-.216-.106-.446-.16-.663l.032-.025Z`];\n        return drawPath(targetEl, {mode: \"fill\", template, SVGWidth: 200, SVGHeight: 24, position: \"bottom\"});\n    },\n    double: targetEl => {\n        const template = (w, h) => [\n            `M 0,${h * 0.9} h ${w}`,\n            `M 0,${h * 1.1} h ${w}`,\n        ];\n        return drawPath(targetEl, {mode: \"free\", template});\n    },\n    wavy: targetEl => {\n        const template = (w, h) => [\n            `c ${w / 4},0 ${w / 4},-${h / 2} ${w / 2},-${h / 2}` +\n            `c ${w / 4},0 ${w / 4},${h / 2} ${w / 2},${h / 2}`\n        ];\n        return drawPath(targetEl, {mode: \"pattern\", template});\n    },\n    circle_1: targetEl => {\n        const template = (w, h) => [\n            `M ${w / 2.88},${h / 1.1} C ${w / 1.1},${h / 1.05} ${w * 1.05},${h / 1.1} ${w * 1.023},${h / 2.32}` +\n            `C ${w}, ${h / 14.6} ${w / 1.411},0 ${w / 2},0 S -2,${h / 14.6} -2,${h / 2.2}` +\n            `S ${w / 4.24},${h} ${w / 1.36},${h * 1.04}`\n        ];\n        return drawPath(targetEl, {mode: \"free\", template});\n    },\n    circle_2: targetEl => {\n        const template = (w, h) => [`M112.58 21.164h18.516c-.478-.176-1.722-.64-2.967-1.105.101-.401.214-.803.315-1.192 12.255 2.912 24.561 5.573 36.716 8.823 5.896 1.582 ` +\n        `11.628 3.967 17.171 6.527 10.433 4.832 14.418 14.22 16.479 24.739.377 1.92.566 3.878.83 5.823 2.212 15.94-5.858 23.986-21.595 33.813-.993.615-2.288.79-3.181 1.494` +\n        `-14.229 11.308-31.412 14.32-48.608 17.107-29.01 4.694-57.431 2.209-84.91-8.372-8.145-3.138-16.164-6.853-23.706-11.22C6.176 90.986 1.16 80.053.193 67.25c-1.798-23.` +\n        `809 9.025-42.485 30.356-53.304C44.678 6.793 59.8 3.367 75.45 2.375 90.583 1.42 105.793.379 120.927.78c16.089.427 32.041 3.05 46.911 9.84 2.074.941 3.67 2.912 4.91` +\n        `5 5.083-9.73-1.443-19.433-2.987-29.175-4.305-4.89-.665-9.842-1.067-14.77-1.33-23.82-1.28-47.376.514-70.391 7.003a133.771 133.771 0 0 0-22.639 8.648c-17.9 8.786-27` +\n        `.616 26.935-25.567 46.364.666 6.263 3.507 11.133 9.05 14.308 26.862 15.401 55.748 21.965 86.645 19.819 15.561-1.08 31.01-2.787 45.767-8.284 11.099-4.142 21.658-9.` +\n        `25 30.595-17.195 9.779-8.698 11.715-18.55 5.669-30.249-1.131-2.196-3.256-4.079-5.33-5.56-7.981-5.736-17.773-7.48-26.459-11.534-13.249-6.175-27.541-6.916-41.343-10` +\n        `.167-.817-.188-1.571-.64-2.35-.966.037-.364.088-.728.125-1.092Z`];\n        return drawPath(targetEl, {mode: \"fill\", template, SVGWidth: 200, SVGHeight: 120});\n    },\n    circle_3: targetEl => {\n        const template = (w, h) => [`M78.653 89.204c-14.815 0-29.403-1.096-43.354-4.698-5.227-1.346-10.407-3.069-14.997-5.199-22.996-10.649-27.04-28.502-9.135-43.035 12.18` +\n        `-9.866 26.813-18.04 43.355-24.242C88.515-.718 124.19-3.725 161.228 4.889c13.224 3.07 24.449 8.268 31.902 16.662 8.862 9.992 9.453 20.422 0 30.068-5.817 5.889-13.2` +\n        `24 11.37-21.359 15.786-27.176 14.752-58.579 21.518-93.072 21.8h-.046Zm3.5-4.228c4.408-.282 11.725-.47 18.86-1.253 30.357-3.351 57.579-11.432 79.211-26.842 5.362-3` +\n        `.82 10.134-8.832 12.27-13.875 2.545-5.982 5.817-13.311-6.226-17.352-.454-.156-.727-.563-1.045-.845-10.771-9.146-25.086-14.157-41.719-15.348-39.674-2.85-76.62 3.19` +\n        `5-109.66 18.762-8.18 3.883-15.497 9.177-21.359 14.752-9.725 9.27-8.044 19.889 3.727 28.032 4.862 3.383 10.997 6.233 17.269 8.237 14.406 4.605 30.04 5.544 48.58 5.` +\n        `763l.092-.03ZM130.37 3.573c-24.813-1.88-48.263 1.378-70.44 9.146 22.814-5.481 46.172-9.02 70.44-9.146Z`];\n        return drawPath(targetEl, {mode: \"fill\", template, SVGWidth: 200, SVGHeight: 90});\n    },\n    over_underline: targetEl => {\n        const template = (w, h) => [\n            `M 0,0 h ${w}`,\n            `M 0,${h} h ${w}`,\n        ];\n        return drawPath(targetEl, {mode: \"free\", template});\n    },\n    scribble_1: targetEl => {\n        const template = (w, h) => [\n            `M ${w / 2},${h * 0.9} c ${w / 16},0 ${w},1 ${w / 5},1 c 2,0 -${w / 10},-2 -${w / 2},-1` +\n            `c -${w / 20},0 -${w / 5},2 -${w / 5},4 c -2,0 ${w / 10},-1 ${w / 2},${h / 16}` +\n            `c ${w / 25},0 ${w / 10},0 ${w / 5},1 c 0,0 -${w / 10},1 -${w / 8},1` +\n            `c -${w / 40},0 -${w / 16},0 -${w / 4},${h / 22}`\n        ];\n        return drawPath(targetEl, {mode: \"free\", template});\n    },\n    scribble_2: targetEl => {\n        const template = (w, h) => [`M200 3.985c-.228-.332-3.773.541-.01-.006-.811-.037-6.705-1.442-9.978-1.706-1.473.194-2.907.534-4.351.818-1.398.27-2.937.985-4.144.756-` +\n        `9.56-1.782-19.3-1.089-28.955-1.31C118.932 1.767 85.301.942 51.671.45c-13.732-.201-27.492.333-41.233.665C6.561 1.212 3.026 2.363.84 4.838.09 5.684-.262 7.126.223 7` +\n        `.993c.313.554 2.518.79 3.839.728 2.47-.118 4.922-.548 8.096-.936-.96 1.227-1.568 1.865-1.986 2.558-1.368 2.302.029 4 3.203 4.083 24.716.666 49.424 1.4 74.15 2.01 ` +\n        `21.087.52 42.145.34 63.146-1.414 4.495-.374 8.999-.644 14.425-1.026-3.117-1.629-4.723-3.521-8.39-3.535-17.999-.077-36.016-.07-54.005-.534-22.246-.576-44.464-1.58-` +\n        `66.7-2.406-.276-.007-.551-.097-.817-.471 1.016 0 2.033-.021 3.04 0 21.961.506 43.913.998 65.864 1.539 25.249.624 50.47.367 75.642-1.144 5.892-.354 11.765-.93 17.6` +\n        `19-1.54.788-.082 1.416-.99 2.651-1.92Z`];\n        return drawPath(targetEl, {mode: \"fill\", template, SVGWidth: 200, SVGHeight: 17, position: \"bottom\"});\n    },\n    scribble_3: targetEl => {\n        const template = (w, h) => [`M133.953 15.961c7.87.502 15.751.975 23.611 1.522 2.027.141 4.055.44 5.999.79 4.118.727 7.202 4.977 2.53 6.707.606.293 1.181.564 1.902.` +\n        `908-8.477 2.069-17.267 2.65-26.203 2.818-19.023.361-38.056.603-57.068 1.088-13.807.355-27.572 1.06-41.369 1.545-3.23.113-6.532.096-9.73-.147-1.548-.118-3.492-.721` +\n        `-4.234-1.42-.93-.88-1.484-2.199-.93-3.1.397-.655 2.812-1.263 4.41-1.33 6.397-.277 12.825-.333 19.243-.474 26.976-.592 53.942-1.156 80.919-1.804 3.742-.09 7.452-.5` +\n        `92 11.173-.908 0-.174-.01-.35-.021-.524-2.717-.197-5.435-.53-8.163-.575-21.865-.383-43.741-1.009-65.607-.936-11.34.04-22.65 1.432-34 2.047-6.898.377-13.88.732-20.` +\n        `779.569-7.044-.17-9.406-3.568-5.34-6.742 3.428-2.677 7.567-4.391 13.984-4.757 16.441-.93 32.798-2.26 49.219-3.27 14.162-.868 28.366-1.516 42.549-2.266.586-.034 1.` +\n        `15-.147 1.641-.45-5.006 0-10.023-.012-15.029.01-1.077 0-2.154.186-3.24.192-18.793.18-37.596.355-56.389.507-10.672.085-21.343.13-32.014.153a65.89 65.89 0 0 1-6.167` +\n        `-.277C1.787 5.555-.02 4.247 0 2.59 0 1.384.89.72 3.293.742c5.874.056 11.748.124 17.622.09C41.045.708 61.186.409 81.317.42c28.408.012 56.827.158 85.225.417 8.686.0` +\n        `8 17.35.7 26.015 1.122 3.23.158 5.832.902 7.024 2.678 1.055 1.572.125 2.21-2.875 1.95a30.51 30.51 0 0 0-2.268-.107c-.397 0-.805.073-1.557.146.721.451 1.306.767 1.` +\n        `777 1.128 2.926 2.238 1.641 4.013-3.272 4.369-13.483.958-26.966 1.91-40.459 2.767-3.334.214-6.752 0-10.118.085-2.31.062-4.609.299-6.909.462l.042.519.011.005Z`];\n        return drawPath(targetEl, {mode: \"fill\", template, SVGWidth: 200, SVGHeight: 32, position: \"bottom\"});\n    },\n    scribble_4: targetEl => {\n        const template = (w, h) => [`M96.414 17.157c1.34-2.173 2.462-4.075 3.649-5.944 2.117-3.335 5.528-4.302 9.372-2.694 3.962 1.651 4.89 3.575 3.908 8.073-.205.967-.388` +\n        ` 1.934-.022 3.118 1.513-3.075 3.013-6.15 4.557-9.203 1.306-2.586 4.297-3.433 7.859-2.195 2.765.968 4.395 2.706 3.564 5.922-.529 2.054-1.005 4.118-.918 6.487.463-.` +\n        `859 1.015-1.685 1.371-2.586 1.447-3.673 3.002-7.324 4.2-11.083.896-2.792 2.192-3.955 5.323-3.564 4.772.598 7.049 3.412 5.84 7.986-.626 2.38-1.22 4.77-1.144 7.486.` +\n        `745-1.358 1.544-2.683 2.213-4.074a138.72 138.72 0 0 0 2.926-6.487c2.376-5.66 3.12-4.704 8.724-3.618 3.552.685 5.063 4.031 4.34 7.997-.616 3.423-1.166 6.856-1.749 ` +\n        `10.29l.95.358c.993-2.151 2.062-4.27 2.958-6.454.594-1.456.886-3.042 1.403-4.53 2.43-6.911 2.43-6.813 9.566-5.542.928.163 2.656-.967 3.078-1.923.992-2.26 2.332-2.7` +\n        `16 4.523-2.097 4.297 1.206 8.659 2.184 12.945 3.444 2.796.826 4.319 2.988 4.135 5.889-.173 2.684-.961 5.324-1.274 8.008-.734 6.4-1.361 12.799-2.019 19.21-.065.673` +\n        `.043 1.38-.097 2.031-.551 2.477-.41 5.465-3.476 6.421-2.311.717-6.489-2.194-7.644-5.03-.206-.5-.357-1.01-.918-2.63-1.22 3.27-2.073 5.629-2.991 7.965-2.095 5.345-3` +\n        `.66 5.954-8.874 3.705-.853-.37-2.354-.783-2.786-.359-3.163 3.075-5.971 1.217-8.853-.358-.378-.207-.81-.316-1.188-.457-5.851 7.65-12.502 4.596-15.061-3.944-1.543 3` +\n        `.042-2.883 5.726-4.265 8.399-3.357 6.53-7.783 6.975-12.47 1.25-.485-.587-.992-1.152-1.511-1.75-5.647 6.715-12.848 2.293-15.19-6.063-1.253 2.25-2.257 3.88-3.099 5.` +\n        `596-1.285 2.64-2.883 4.65-6.23 3.868-3.498-.826-6.532-4.085-6.65-7.225-.054-1.424 0-2.847-.475-4.433-1.393 2.879-2.71 5.802-4.19 8.637-3.228 6.204-6.067 6.824-11.` +\n        `67 2.912-.962-.673-2.57-.988-3.704-.728-3.681.837-6.272-.619-8.626-3.248-.691-.783-2.084-1.771-2.807-1.543-4.243 1.347-6.91-.641-9.166-3.836-.378-.543-.8-1.053-1.` +\n        `555-2.031-1.08 2.194-2.008 4.041-2.915 5.9-2.397 4.943-5.528 5.932-10.02 2.835-2.008-1.38-3.713-2.118-6.37-1.738-5.117.728-8.54-3.444-7.762-8.649.227-1.521.378-3.` +\n        `064-.086-4.9-.853 1.369-1.793 2.684-2.548 4.107-2.775 5.259-5.301 5.856-10.074 2.206-.971-.75-1.803-1.674-2.86-2.673-.67.271-1.598 1.043-2.257.858-2.71-.771-5.625` +\n        `-1.423-7.838-3.01-.842-.608-.378-3.683.108-5.465 2.008-7.41 4.232-14.755 6.413-22.11.572-1.945 1.166-3.901 1.943-5.77 1.89-4.52 5.02-5.454 9.145-2.89 1.144.706 2.` +\n        `408 1.217 3.552 1.923 2.364 1.456 4.696 2.988 7.439 4.737C32.423 7.14 37.444 6.64 42.82 10.41c2.602-2.107 1.803-7.17 6.748-6.323 3.369.587 6.478 1.217 7.439 4.878` +\n        ` 2.289-2.281 4.221-5.693 6.877-6.42 2.624-.718 5.992 1.26 9.599 2.216-.044.054.636-.565.96-1.348 1.048-2.499 2.883-3.4 5.42-2.825 2.775.62 5.474 1.304 6.284 4.76.` +\n        `216.89 1.285 2.042 2.159 2.248 7.58 1.793 7.6 1.739 8.108 9.55v.012Z`];\n        return drawPath(targetEl, {mode: \"fill\", template, SVGWidth: 200, SVGHeight: 61});\n    },\n    jagged: targetEl => {\n        const template = (w, h) => [\n            `q ${4 * w / 3} -${2 * w / 3} ${2 * w / 3} 0` +\n            `c -${w / 3} ${w / 3} -${w / 3} ${w / 3} ${w / 3} 0`\n        ];\n        return drawPath(targetEl, {mode: \"pattern\", template});\n    },\n    cross: targetEl => {\n        const template = (w, h) => [\n            `M 0,0 L ${w},${h}`,\n            `M 0,${h} L ${w},0`,\n        ];\n        return drawPath(targetEl, {mode: \"free\", template});\n    },\n    diagonal: targetEl => {\n        const template = (w, h) => [`M 0,${h} L${w},0`];\n        return drawPath(targetEl, {mode: \"free\", template});\n    },\n    strikethrough: targetEl => {\n        return drawPath(targetEl, {mode: \"line\", position: \"center\"});\n    },\n    bold: targetEl => {\n        const template = (w, h) => [`M136.604 41.568c5.373.513 10.746 1.047 16.12 1.479 14.437 1.13 29.327 4.047 42.858-4.294 4.92-3.04 2.346-13.56-2.687-13.395-.825.02-1.` +\n        `635.062-2.46.082.858-3.677-.34-8.3-3.545-9.41 2.655.062 5.309.104 7.963.165 6.863.185 6.863-14.176 0-14.36A1958.994 1958.994 0 0 0 5.263 5.778C-.4 6.169-2.392 18.` +\n        `455 3.84 19.893c9.727 2.24 19.454 4.335 29.214 6.307-1.085 1.09-1.764 2.671-2.023 4.356-.615.061-1.214.102-1.83.164-6.748.74-6.959 14.587 0 14.361l107.42-3.513h-.` +\n        `016Z`];\n        return drawPath(targetEl, {mode: \"fill\", template, SVGWidth: 200, SVGHeight: 46});\n    },\n    bold_1: targetEl => {\n        const template = (w, h) => [`M190.276 34.01c5.618-.25 7.136-6.526 4.444-9.755.037-.25.055-.5.072-.749 7.046-.949 7.01-11.752-.523-11.553-.796.017-1.59.017-2.403.05` +\n        `C196.78 9.573 195.931.8 189.264.983L13.784 5.678c-7.226.2-7.497 9.422-1.499 11.32-2.186 0-4.354 0-6.54-.017-7.696-.05-7.624 11.286 0 11.635 8.22.383 16.423.733 24` +\n        `.643 1.016l-7.823.35c-7.624.349-7.678 11.985 0 11.635 55.915-2.53 111.813-5.077 167.729-7.607h-.018Z`];\n        return drawPath(targetEl, {mode: \"fill\", template, SVGWidth: 200, SVGHeight: 42});\n    },\n    bold_2: targetEl => {\n        const template = (w, h) => [`M193.221 20.193c.555 1.245.863 2.005 1.22 2.734 1.399 2.84 2.758 5.757 1.607 9.509-1.21 3.95-3.651 4.208-6.072 4.314-5.059.212-10.129.` +\n        `152-15.178.592-15.873 1.367-31.737 3.585-47.619 4.238-19.921.82-39.862.638-59.802.486-13.938-.106-27.887-.88-41.825-1.428-4.018-.151-8.046-.47-12.064-.896-2.758-.` +\n        `304-4.772-2.46-6.21-6.182-.645-1.656-1.756-2.993-2.798-4.177-2.768-3.13-5.06-6.38-3.899-12.502C.9 15.226.393 13.16.165 11.307c-.715-5.818.903-9.524 4.722-9.646 10` +\n        `.218-.35 20.437-.38 30.655-.577C51.236.78 66.94-.04 82.635.264c14.652.273 29.296 1.655 43.948 2.643 19.822 1.336 39.643 2.02 59.455-.426.923-.121 1.835-.5 2.758-.` +\n        `622 1.329-.183 2.688-.456 4.008-.274 3.829.501 7.073 5.666 7.192 11.21.09 4.466-1.418 6.213-6.775 7.428v-.03Z`];\n        return drawPath(targetEl, {mode: \"fill\", template, SVGWidth: 200, SVGHeight: 43});\n    },\n};\n// Returns the width of the DOMRect object.\nexport const getDOMRectWidth = el => el.getBoundingClientRect().width;\n\n/**\n * Draws one or many SVG paths using templates of path shape commands.\n *\n * @param {HTMLElement} textEl\n * @param {String} options.mode Specifies how to draw the path:\n * - \"pattern\": repeat the template along the horizontal axis.\n * - \"line\": draw a simple line (we specify the width & position).\n * - \"free\": draw the path shape using the template only.\n * - \"fill\": used for irregular shapes that do not follow the \"stroke\" design.\n * @param {Function} options.template Returns a list of SVG path\n * commands adapted to the container's size.\n * @returns {String[]}\n */\nfunction drawPath(textEl, options) {\n    // Note: cannot use getBoundingClientRect as we want to be able to draw\n    // text highlights in snippets/add page dialogs where iframe is scaled.\n    const width = textEl.offsetWidth;\n    const height = textEl.offsetHeight;\n    options = {...options, width, height};\n    const yStart = options.position === \"center\" ? height / 2 : height;\n\n    switch (options.mode) {\n        case \"pattern\": {\n            let i = 0, d = [];\n            const nbrChars = textEl.textContent.length;\n            const w = width / nbrChars, h = height * 0.2;\n            while (i < nbrChars) {\n                d.push(options.template(w, h));\n                i++;\n            }\n            return buildPath([`M 0,${yStart} ${d.join(\" \")}`], options);\n        }\n        case \"line\": {\n            return buildPath([`M 0,${yStart} h ${width}`], options);\n        }\n    }\n    return buildPath(options.template(width, height), options);\n}\n\n/**\n * Used to build the SVG <path/>, it should mainly adapt it to take into\n * consideration some cases where the shape is a \"filled path\" instead\n * of a single line stroke.\n *\n * @param {String[]} templates\n * @param {Object} options\n * @returns {Element[]}\n */\nfunction buildPath(templates, options) {\n    return templates.map(d => {\n        const path = document.createElementNS(\"http://www.w3.org/2000/svg\", \"path\");\n        path.setAttribute(\"stroke-width\", \"var(--text-highlight-width)\");\n        path.setAttribute(\"stroke\", \"var(--text-highlight-color)\");\n        path.setAttribute(\"stroke-linecap\", \"round\");\n        if (options.mode === \"fill\") {\n            let wScale = options.width / options.SVGWidth;\n            let hScale = options.height / options.SVGHeight;\n            const transforms = [];\n            if (options.position === \"bottom\") {\n                hScale *= 0.3;\n                transforms.push(`translate(0 ${options.height * 0.8})`);\n            }\n            transforms.push(`scale(${wScale}, ${hScale})`);\n            path.setAttribute(\"fill\", \"var(--text-highlight-color)\");\n            path.setAttribute(\"transform\", transforms.join(\" \"));\n        }\n        path.setAttribute(\"d\", d);\n        return path;\n    });\n}\n\n/**\n * Returns a new highlight SVG adapted to the text container.\n *\n * @param {HTMLElement} textEl\n * @param {String} highlightID\n */\nexport function drawTextHighlightSVG(textEl, highlightID) {\n    const svg = document.createElementNS(\"http://www.w3.org/2000/svg\", \"svg\");\n    svg.setAttribute(\"fill\", \"none\");\n    svg.classList.add(\n        \"o_text_highlight_svg\",\n        // Identifies DOM content that should not be merged by the editor, even\n        // on identical parents.\n        \"o_content_no_merge\",\n        \"position-absolute\",\n        \"overflow-visible\",\n        \"top-0\",\n        \"start-0\",\n        \"w-100\",\n        \"h-100\",\n        \"pe-none\");\n    _textHighlightFactory[highlightID](textEl).forEach(pathEl => {\n        pathEl.classList.add(`o_text_highlight_path_${highlightID}`);\n        svg.appendChild(pathEl);\n    });\n    return svg;\n}\n\n/**\n * Divides the content of a text container into multiple\n * `.o_text_highlight_item` units, and applies the highlight\n * on each unit.\n *\n * @param {HTMLElement} topTextEl\n * @param {String} highlightID\n */\nexport function applyTextHighlight(topTextEl, highlightID) {\n    // Don't reapply the effects to a highlighted text.\n    if (topTextEl.querySelector(\".o_text_highlight_item\")) {\n        return;\n    }\n    const style = window.getComputedStyle(topTextEl);\n    if (!style.getPropertyValue(\"--text-highlight-width\")) {\n        // The default value for `--text-highlight-width` is 0.1em.\n        topTextEl.style.setProperty(\"--text-highlight-width\", `${Math.round(parseFloat(style.fontSize) * 0.1)}px`);\n    }\n    const lines = [];\n    let lineIndex = 0;\n    const nodeIsBR = node => node.nodeName === \"BR\";\n    const isRTL = el => window.getComputedStyle(el).direction === \"rtl\";\n\n    [...topTextEl.childNodes].forEach(child => {\n        // We consider `<br/>` tags as full text lines to ease\n        // excluding them when the highlight is applied on the DOM.\n        if (nodeIsBR(child)) {\n            lines[++lineIndex] = [child];\n            return lineIndex++;\n        }\n        const textLines = splitNodeLines(child);\n\n        // Special case: The text lines detection code in `splitNodeLines()`\n        // (based on `getClientRects()`) can't handle a situation when a line\n        // exactly ends with the current child node. We need to handle this\n        // manually by checking if the current child node is the last one in\n        // the line (taking into account the RTL direction).\n        // TODO: Improve this.\n        let lastNodeInLine = false;\n        if (child.textContent && child.nextSibling?.textContent) {\n            const range = document.createRange();\n            const lastCurrentText = selectAllTextNodes(child).at(-1);\n            range.setStart(lastCurrentText, lastCurrentText.length - 1);\n            range.setEnd(lastCurrentText, lastCurrentText.length);\n            // Get the \"END\" position of the last text node in current child.\n            const currentEnd = range.getBoundingClientRect()[isRTL(topTextEl) ? \"left\" : \"right\"];\n            const firstnextText = selectAllTextNodes(child.nextSibling)[0];\n            range.setStart(firstnextText, 0);\n            range.setEnd(firstnextText, 1);\n            // Get the \"START\" position of the first text node in the next\n            // sibling.\n            const nextStart = range.getBoundingClientRect()[isRTL(topTextEl) ? \"right\" : \"left\"];\n            // The next sibling starts before the end of the current node\n            // => Line break detected.\n            lastNodeInLine = nextStart + 1 < currentEnd;\n        }\n\n        // for each text line detected, we add the content as new\n        // line and adjust the line index accordingly.\n        textLines.map((node, i, {length}) => {\n            if (!lines[lineIndex]) {\n                lines[lineIndex] = [];\n            }\n            lines[lineIndex].push(node);\n            if (i !== length - 1 || lastNodeInLine) {\n                lineIndex++;\n            }\n        });\n    });\n    topTextEl.replaceChildren(...lines.map(textLine => {\n        // First we add text content to be able to build svg paths\n        // correctly (`<br/>` tags are excluded).\n        return nodeIsBR(textLine[0]) ? textLine[0] :\n            createHighlightContainer(textLine);\n    }));\n    // Build and set highlight SVGs.\n    [...topTextEl.querySelectorAll(\".o_text_highlight_item\")].forEach(container => {\n        container.append(drawTextHighlightSVG(container, highlightID || getCurrentTextHighlight(topTextEl)));\n    });\n    topTextEl.dispatchEvent(new Event(\"text_highlight_added\", { bubbles: true }));\n}\n\n/**\n * Used to rollback the @see applyTextHighlight behaviour.\n *\n * @param {HTMLElement} topTextEl\n */\nexport function removeTextHighlight(topTextEl) {\n    topTextEl.dispatchEvent(new Event(\"text_highlight_remove\", { bubbles: true }));\n    // Simply replace every `<span class=\"o_text_highlight_item\">\n    // textNode1 [textNode2,...]<svg .../></span>` by `textNode1\n    // [textNode2,...]`.\n    [...topTextEl.querySelectorAll(\".o_text_highlight_item\")].forEach(unit => {\n        unit.after(...[...unit.childNodes].filter((node) => node.tagName !== \"svg\"));\n        unit.remove();\n    });\n    // Prevents incorrect text lines detection on the next updates.\n    let child = topTextEl.firstElementChild;\n    while (child) {\n        let next = child.nextElementSibling;\n        // Merge identical elements.\n        if (next && next === child.nextSibling && child.cloneNode().isEqualNode(next.cloneNode())) {\n            child.replaceChildren(...child.childNodes, ...next.childNodes);\n            next.remove();\n        } else {\n            child = next;\n        }\n    }\n    topTextEl.normalize();\n}\n\n/**\n * Used to change or adjust the highlight effect when it's needed (E.g. on\n * window / text container \"resize\").\n *\n * @param {HTMLElement} textEl The top text highlight element.\n * @param {String} highlightID The new highlight to apply (or the old one\n * if we just want to adapt the effect).\n */\nexport function switchTextHighlight(textEl, highlightID) {\n    highlightID = highlightID || getCurrentTextHighlight(textEl);\n    const ownerDocument = textEl.ownerDocument;\n    const sel = ownerDocument.getSelection();\n    const restoreSelection = sel.rangeCount === 1 && textEl.contains(sel.anchorNode);\n    let rangeCollapsed,\n    cursorEndPosition = 0,\n    rangeSize = 0;\n\n    // Because of text highlight adaptations, the selection offset will\n    // be lost, which will cause issues when typing and deleting text...\n    // The goal here is to preserve the selection to restore it for the\n    // new elements after the update when it's needed.\n    if (restoreSelection) {\n        const range = sel.getRangeAt(0);\n        rangeSize = range.toString().length;\n        rangeCollapsed = range.collapsed;\n        // We need the position related to the `.o_text_highlight` element.\n        const globalRange = range.cloneRange();\n        globalRange.selectNodeContents(textEl);\n        globalRange.setEnd(range.endContainer, range.endOffset);\n        cursorEndPosition = globalRange.toString().length;\n    }\n\n    // Set the new text highlight effect.\n    if (highlightID) {\n        removeTextHighlight(textEl);\n        applyTextHighlight(textEl, highlightID);\n    }\n\n    // Restore the old selection.\n    if (restoreSelection && cursorEndPosition) {\n        if (rangeCollapsed) {\n            const selectionOffset = getOffsetNode(textEl, cursorEndPosition);\n            OdooEditorLib.setSelection(...selectionOffset, ...selectionOffset);\n        } else {\n            OdooEditorLib.setSelection(\n                ...getOffsetNode(textEl, cursorEndPosition - rangeSize),\n                ...getOffsetNode(textEl, cursorEndPosition)\n            );\n        }\n    }\n}\n\n/**\n * Used to wrap text nodes in a single \"text highlight\" unit.\n *\n * @param {Node[]} nodes\n * @returns {HTMLElement} The one line text element that should contain\n * the highlight SVG.\n */\nfunction createHighlightContainer(nodes) {\n    const highlightContainer = document.createElement(\"span\");\n    highlightContainer.className = \"o_text_highlight_item\";\n    highlightContainer.append(...nodes);\n    return highlightContainer;\n}\n\n/**\n * Used to get the current text highlight id from the top `.o_text_highlight`\n * container class.\n *\n * @param {HTMLElement} el\n * @returns {String}\n */\nexport function getCurrentTextHighlight(el) {\n    const topTextEl = el.closest(\".o_text_highlight\");\n    const match = topTextEl?.className.match(/o_text_highlight_(?<value>[\\w]+)/);\n    let highlight = \"\";\n    if (match) {\n        highlight = match.groups.value;\n    }\n    return highlight;\n}\n\n/**\n * Returns a list of detected lines in the content of a text node.\n *\n * @param {Node} node\n */\nfunction splitNodeLines(node) {\n    const isTextContainer = node.childNodes.length === 1\n        && node.firstChild.nodeType === Node.TEXT_NODE;\n    if (node.nodeType !== Node.TEXT_NODE && !isTextContainer) {\n        return [node];\n    }\n    const text = node.textContent;\n    const textNode = isTextContainer ? node.firstChild : node;\n    const lines = [];\n    const range = document.createRange();\n    let i = -1;\n    while (++i < text.length) {\n        range.setStart(textNode, 0);\n        range.setEnd(textNode, i + 1);\n        const clientRects = range.getClientRects().length || 1;\n        const lineIndex = clientRects - 1;\n        const currentText = lines[lineIndex];\n        lines[lineIndex] = (currentText || \"\") + text.charAt(i);\n    }\n    // Return the original node when no lines were detected.\n    if (lines.length === 1) {\n        return [node];\n    }\n    return lines.map(line => {\n        if (isTextContainer) {\n            const wrapper = node.cloneNode();\n            wrapper.appendChild(document.createTextNode(line));\n            return wrapper;\n        }\n        return document.createTextNode(line);\n    });\n}\n\n/**\n * Get all text nodes inside a parent DOM element.\n *\n * @param {Node} topNode\n * @returns {Node[]} List of text \"childNodes\" or the element itself\n * (if it's a text node).\n */\nexport function selectAllTextNodes(topNode) {\n    const textNodes = [];\n    const selectTextNodes = (node) => {\n        if (node.nodeType === Node.TEXT_NODE) {\n            textNodes.push(node);\n        } else {\n            [...node.childNodes].forEach(child => selectTextNodes(child));\n        }\n    };\n    selectTextNodes(topNode);\n    return textNodes;\n}\n\n/**\n * Used to get the node of a text element in which a selection starts/ends.\n *\n * @param {HTMLElement} textEl The parent text element.\n * @param {Number} offset The selection offset in parent element.\n * @returns {[Node, Number]} The node found in the cursor position\n * and the new offset compared to that node.\n */\nexport function getOffsetNode(textEl, offset) {\n    let index = 0,\n    offsetNode;\n    for (const node of selectAllTextNodes(textEl)) {\n        const stepLength = node.textContent.length;\n        if (index + stepLength < offset - 1) {\n            index += stepLength;\n        } else {\n            offsetNode = node;\n            break;\n        }\n    }\n    return [offsetNode, offset - index];\n}\n", "export const ObservingCookieWidgetMixin = {\n    /**\n     * Updates the element's iframe according to whether the cookies should be\n     * approved (marked by `_post_processing_att` server-side).\n     *\n     * @private\n     * @param {HTMLElement} rootEl - root element of the widget.\n     * @param {string} src - src to set on the iframe.\n     */\n    _manageIframeSrc(rootEl, src) {\n        const iframeEl = rootEl.querySelector(\"iframe\");\n        if (!rootEl.dataset.needCookiesApproval) {\n            iframeEl.setAttribute(\"src\", src);\n        } else {\n            iframeEl.dataset.nocookieSrc = src;\n            iframeEl.setAttribute(\"src\", \"about:blank\");\n            $(iframeEl).trigger(\"add_cookies_warning\");\n        }\n    },\n};\n", "/** @odoo-module */\n\nimport PublicWidget from \"@web/legacy/js/public/public_widget\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport const PurchaseDatePicker = PublicWidget.Widget.extend({\n    selector: \".o-purchase-datetimepicker\",\n    disabledInEditableMode: true,\n\n    /**\n     * @override\n     */\n    start() {\n        this.disableDateTimePicker = this.call(\"datetime_picker\", \"create\", {\n            target: this.el,\n            onChange: (newDate) => {\n                const { accessToken, orderId, lineId } = this.el.dataset;\n                rpc(`/my/purchase/${orderId}/update?access_token=${accessToken}`, {\n                    [lineId]: newDate.toISODate(),\n                });\n            },\n            pickerProps: {\n                type: \"date\",\n                value: luxon.DateTime.fromISO(this.el.dataset.value),\n            },\n        }).enable();\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this.disableDateTimePicker();\n        return this._super(...arguments);\n    },\n});\n\nPublicWidget.registry.PurchaseDatePicker = PurchaseDatePicker;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport PortalSidebar from \"@portal/js/portal_sidebar\";\nimport { uniqueId } from \"@web/core/utils/functions\";\n\npublicWidget.registry.PurchasePortalSidebar = PortalSidebar.extend({\n    selector: \".o_portal_purchase_sidebar\",\n\n    /**\n     * @constructor\n     */\n    init: function (parent, options) {\n        this._super.apply(this, arguments);\n        this.authorizedTextTag = [\"em\", \"b\", \"i\", \"u\"];\n        this.spyWatched = $('body[data-target=\".navspy\"]');\n        this.orm = this.bindService(\"orm\");\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        var def = this._super.apply(this, arguments);\n        var $spyWatcheElement = this.$el.find('[data-id=\"portal_sidebar\"]');\n        this._setElementId($spyWatcheElement);\n        // Nav Menu ScrollSpy\n        this._generateMenu();\n        return def;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //---------------------------------------------------------------------------\n\n    /**\n     * create an unique id and added as a attribute of spyWatched element\n     *\n     * @private\n     * @param {string} prefix\n     * @param {Object} $el\n     *\n     */\n    _setElementId: function (prefix, $el) {\n        var id = uniqueId(prefix);\n        this.spyWatched.find($el).attr(\"id\", id);\n        return id;\n    },\n    /**\n     * generate the new spy menu\n     *\n     * @private\n     *\n     */\n    _generateMenu: function () {\n        var self = this,\n            lastLI = false,\n            lastUL = null,\n            $bsSidenav = this.$el.find(\".bs-sidenav\");\n\n        $(\"#quote_content [id^=quote_header_], #quote_content [id^=quote_]\", this.spyWatched).attr(\n            \"id\",\n            \"\"\n        );\n        this.spyWatched\n            .find(\"#quote_content h2, #quote_content h3\")\n            .toArray()\n            .forEach((el) => {\n                var id, text;\n                switch (el.tagName.toLowerCase()) {\n                    case \"h2\":\n                        id = self._setElementId(\"quote_header_\", el);\n                        text = self._extractText($(el));\n                        if (!text) {\n                            break;\n                        }\n                        lastLI = $(\"<li class='nav-item'>\")\n                            .append(\n                                $(\n                                    '<a class=\"nav-link p-0\" style=\"max-width: 200px;\" href=\"#' +\n                                        id +\n                                        '\"/>'\n                                ).text(text)\n                            )\n                            .appendTo($bsSidenav);\n                        lastUL = false;\n                        break;\n                    case \"h3\":\n                        id = self._setElementId(\"quote_\", el);\n                        text = self._extractText($(el));\n                        if (!text) {\n                            break;\n                        }\n                        if (lastLI) {\n                            if (!lastUL) {\n                                lastUL = $(\"<ul class='nav flex-column'>\").appendTo(lastLI);\n                            }\n                            $(\"<li class='nav-item'>\")\n                                .append(\n                                    $(\n                                        '<a class=\"nav-link p-0\" style=\"max-width: 200px;\" href=\"#' +\n                                            id +\n                                            '\"/>'\n                                    ).text(text)\n                                )\n                                .appendTo(lastUL);\n                        }\n                        break;\n                }\n                el.setAttribute(\"data-anchor\", true);\n            });\n        this.trigger_up(\"widgets_start_request\", { $target: $bsSidenav });\n    },\n    /**\n     * extract text of menu title for sidebar\n     *\n     * @private\n     * @param {Object} $node\n     *\n     */\n    _extractText: function ($node) {\n        var self = this;\n        var rawText = [];\n        Array.from($node.contents()).forEach((el) => {\n            var current = $(el);\n            if ($.trim(current.text())) {\n                var tagName = current.prop(\"tagName\");\n                if (\n                    typeof tagName === \"undefined\" ||\n                    (typeof tagName !== \"undefined\" &&\n                        self.authorizedTextTag.includes(tagName.toLowerCase()))\n                ) {\n                    rawText.push($.trim(current.text()));\n                }\n            }\n        });\n        return rawText.join(\" \");\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { parseDate } from '@web/core/l10n/dates';\n\npublicWidget.registry.ProjectRatingImage = publicWidget.Widget.extend({\n    selector: '.o_portal_project_rating .o_rating_image',\n\n    /**\n     * @override\n     */\n    start: function () {\n        this.$el.popover({\n            placement: 'bottom',\n            trigger: 'hover',\n            html: true,\n            content: function () {\n                var $elem = $(this);\n                var id = $elem.data('id');\n                var ratingDate = $elem.data('rating-date');\n                var baseDate = parseDate(ratingDate);\n                var duration = baseDate.toRelative();\n                var $rating = $('#rating_' + id);\n                $rating.find('.rating_timeduration').text(duration);\n                return $rating.html();\n            },\n        });\n        return this._super.apply(this, arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from '@web/core/l10n/translation';\n\nimport PaymentForm from '@payment/js/payment_form';\n\nPaymentForm.include({\n    events: Object.assign({}, PaymentForm.prototype.events || {}, {\n        'change input[name=\"o_donation_amount\"]': '_updateAmount',\n        'focus input[name=\"amount\"]': '_updateAmount',\n        'focus input[name=\"o_donation_amount\"]': '_updateAmount',\n    }),\n\n\n    // #=== EVENT HANDLERS ===#\n\n    /**\n     * Update the amount in the payment context with the user input.\n     *\n     * @private\n     * @param {Event} ev\n     * @return {void}\n     */\n    _updateAmount(ev) {\n        if (ev.target.value >= 0) {\n            this.paymentContext.amount = ev.target.value;\n            const otherAmountEl = this.el.querySelector(\"#other_amount\");\n            if (ev.target.id === \"other_amount_value\" && otherAmountEl) {\n                otherAmountEl.value = ev.target.value;\n            }\n            if (ev.target.id === \"other_amount\" || ev.target.id === \"other_amount_value\") {\n                this.el.querySelectorAll('input[name=\"o_donation_amount\"][type=\"radio\"]').forEach((radioEl) => {\n                    radioEl.checked = false;\n                });\n            } else if (ev.target.name === \"o_donation_amount\" && otherAmountEl) {\n                otherAmountEl.checked = false;\n            }\n        }\n    },\n\n    // #=== PAYMENT FLOW ===#\n\n    /**\n     * Perform some validations for donations before processing the payment flow.\n     *\n     * @override method from @payment/js/payment_form\n     * @private\n     * @param {string} providerCode - The code of the selected payment option's provider.\n     * @param {number} paymentOptionId - The id of the selected payment option.\n     * @param {string} paymentMethodCode - The code of the selected payment method, if any.\n     * @param {string} flow - The payment flow of the selected payment option.\n     * @return {void}\n     */\n    async _initiatePaymentFlow(providerCode, paymentOptionId, paymentMethodCode, flow) {\n        if (document.querySelector('.o_donation_payment_form')) {\n            const errorFields = {};\n            if (!this.el.querySelector('input[name=\"email\"]').checkValidity()) {\n                errorFields['email'] = _t(\"Email is invalid\");\n            }\n            const mandatoryFields = {\n                'name': _t('Name'),\n                'email': _t('Email'),\n                'country_id': _t('Country'),\n            };\n            for (const id in mandatoryFields) {\n                const fieldEl = this.el.querySelector(`input[name=\"${id}\"],select[name=\"${id}\"]`);\n                fieldEl.classList.remove('is-invalid');\n                Popover.getOrCreateInstance(fieldEl)?.dispose();\n                if (!fieldEl.value.trim()) {\n                    errorFields[id] = _t(\"Field '%s' is mandatory\", mandatoryFields[id]);\n                }\n            }\n            if (Object.keys(errorFields).length) {\n                for (const id in errorFields) {\n                    const fieldEl = this.el.querySelector(\n                        `input[name=\"${id}\"],select[name=\"${id}\"]`\n                    );\n                    fieldEl.classList.add('is-invalid');\n                    Popover.getOrCreateInstance(fieldEl, {\n                        content: errorFields[id],\n                        placement: 'top',\n                        trigger: 'hover',\n                    });\n                }\n                this._displayErrorDialog(\n                    _t(\"Payment processing failed\"),\n                    _t(\"Some information is missing to process your payment.\")\n                );\n                this._enableButton();\n                return;\n            }\n        }\n        await this._super(...arguments);\n    },\n\n    /**\n     * Add params used by the donation snippet for the RPC to the transaction route.\n     *\n     * @override method from @payment/js/payment_form\n     * @private\n     * @return {object} The extended transaction route params.\n     */\n    _prepareTransactionRouteParams() {\n        const transactionRouteParams = this._super(...arguments);\n        return document.querySelector('.o_donation_payment_form')\n            ? {\n            ...transactionRouteParams,\n            partner_id: parseInt(this.paymentContext['partnerId']),\n            currency_id: this.paymentContext['currencyId']\n                    ? parseInt(this.paymentContext['currencyId']) : null,\n            reference_prefix:this.paymentContext['referencePrefix']?.toString(),\n            partner_details: {\n                name: this.el.querySelector('input[name=\"name\"]').value,\n                email: this.el.querySelector('input[name=\"email\"]').value,\n                country_id: this.el.querySelector('select[name=\"country_id\"]').value,\n            },\n            donation_comment: this.el.querySelector('#donation_comment').value,\n            donation_recipient_email: this.el.querySelector(\n                'input[name=\"donation_recipient_email\"]'\n            ).value,\n        } : transactionRouteParams;\n    },\n\n});\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\n\npublicWidget.registry.WebsitePaymentDonation = publicWidget.Widget.extend({\n    selector: '.o_donation_payment_form',\n    events: {\n        'focus .o_amount_input': '_onFocusAmountInput',\n        'change #donation_comment_checkbox': '_onChangeDonationComment'\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onFocusAmountInput(ev) {\n        const otherAmountEl = this.el.querySelector(\"#other_amount\");\n        if (otherAmountEl) {\n            otherAmountEl.checked = true;\n        }\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onChangeDonationComment(ev) {\n        const donationCommentEl = this.el.querySelector('#donation_comment');\n        const checked = ev.currentTarget.checked;\n        donationCommentEl.classList.toggle('d-none', !checked);\n        if (!checked) {\n            donationCommentEl.value = \"\";\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { ReCaptcha } from \"@google_recaptcha/js/recaptcha\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npublicWidget.registry.follow = publicWidget.Widget.extend({\n    selector: '#wrapwrap',\n    selectorHas: '.js_follow',\n    disabledInEditableMode: false,\n\n    init() {\n        this._super(...arguments);\n        this._recaptcha = new ReCaptcha();\n        this.notification = this.bindService(\"notification\");\n    },\n\n    async willStart() {\n        return this._recaptcha.loadLibs();\n    },\n\n    /**\n     * @override\n     */\n    start: function () {\n        var self = this;\n        this.isUser = false;\n        var $jsFollowEls = this.$el.find('.js_follow');\n\n        var always = function (data) {\n            self.isUser = data[0].is_user;\n            const $jsFollowToEnable = $jsFollowEls.filter(function () {\n                const model = this.dataset.object;\n                return model in data[1] && data[1][model].includes(parseInt(this.dataset.id));\n            });\n            self._toggleSubscription(true, data[0].email, $jsFollowToEnable);\n            self._toggleSubscription(false, data[0].email, $jsFollowEls.not($jsFollowToEnable));\n            $jsFollowEls.removeClass('d-none');\n        };\n\n        const records = {};\n        for (const el of $jsFollowEls) {\n            const model = el.dataset.object;\n            if (!(model in records)) {\n                records[model] = [];\n            }\n            records[model].push(parseInt(el.dataset.id));\n        }\n\n        rpc('/website_mail/is_follower', {\n            records: records,\n        }).then(always, always);\n\n        // not if editable mode to allow designer to edit\n        if (!this.editableMode) {\n            $('.js_follow > .d-none').removeClass('d-none');\n            this.$el.find('.js_follow_btn, .js_unfollow_btn').on('click', function (event) {\n                event.preventDefault();\n                self._onClick(event);\n            });\n        }\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Toggles subscription state for every given records.\n     *\n     * @private\n     * @param {boolean} follow\n     * @param {string} email\n     * @param {jQuery} $jsFollowEls\n     */\n    _toggleSubscription: function (follow, email, $jsFollowEls) {\n        if (follow) {\n            this._updateSubscriptionDOM(follow, email, $jsFollowEls);\n        } else {\n            for (const el of $jsFollowEls) {\n                const follow = !email && el.getAttribute('data-unsubscribe');\n                this._updateSubscriptionDOM(follow, email, $(el));\n            }\n        }\n    },\n    /**\n     * Updates subscription DOM for every given records.\n     * This should not be called directly, use `_toggleSubscription`.\n     *\n     * @private\n     * @param {boolean} follow\n     * @param {string} email\n     * @param {jQuery} $jsFollowEls\n     */\n    _updateSubscriptionDOM: function (follow, email, $jsFollowEls) {\n        $jsFollowEls.find('input.js_follow_email')\n            .val(email || \"\")\n            .attr(\"disabled\", email && (follow || this.isUser) ? \"disabled\" : false);\n        $jsFollowEls.attr(\"data-follow\", follow ? 'on' : 'off');\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    async _onClick(ev) {\n        var self = this;\n        var $jsFollow = $(ev.currentTarget).closest('.js_follow');\n        var $email = $jsFollow.find(\".js_follow_email\");\n\n        if ($email.length && !$email.val().match(/.+@.+/)) {\n            $jsFollow.addClass('o_has_error').find('.form-control, .form-select').addClass('is-invalid');\n            return false;\n        }\n        $jsFollow.removeClass('o_has_error').find('.form-control, .form-select').removeClass('is-invalid');\n\n        var email = $email.length ? $email.val() : false;\n        if (email || this.isUser) {\n            const tokenCaptcha = await this._recaptcha.getToken(\"website_mail_follow\");\n            const token = tokenCaptcha.token;\n\n            if (tokenCaptcha.error) {\n                self.notification.add(tokenCaptcha.error, {\n                    type: \"danger\",\n                    title: _t(\"Error\"),\n                    sticky: true\n                });\n                return false;\n            }\n            rpc(\"/website_mail/follow\", {\n                \"id\": +$jsFollow.data(\"id\"),\n                \"object\": $jsFollow.data(\"object\"),\n                \"message_is_follower\": $jsFollow.attr(\"data-follow\") || \"off\",\n                \"email\": email,\n                \"recaptcha_token_response\": token\n            }).then(function(follow) {\n                self._toggleSubscription(follow, email, $jsFollow);\n            });\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport portalComposer from \"@portal/js/portal_composer\";\n\nvar PortalComposer = portalComposer.PortalComposer;\n\n/**\n * PortalComposer\n *\n * Extends Portal Composer to handle rating submission\n */\nPortalComposer.include({\n    events: Object.assign({}, PortalComposer.prototype.events, {\n        'click .o-mail-Composer-stars i': '_onClickStar',\n        'mousemove .o-mail-Composer-stars i': '_onMoveStar',\n        'mouseleave .o-mail-Composer-stars i': '_onMoveLeaveStar',\n    }),\n\n    /**\n     * @constructor\n     */\n    init: function (parent, options) {\n        this._super.apply(this, arguments);\n\n        // apply ratio to default rating value\n        if (options.default_rating_value) {\n            options.default_rating_value = parseFloat(options.default_rating_value);\n        }\n\n        // default options\n        this.options = Object.assign({\n            'rate_with_void_content': false,\n            'default_message': false,\n            'default_message_id': false,\n            'default_rating_value': 4.0,\n            'force_submit_url': false,\n        }, this.options);\n        this.user_click = false; // user has click or not\n        this._starValue = this.options.default_rating_value;\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        var self = this;\n        return this._super.apply(this, arguments).then(function () {\n            // rating stars\n            self.$input = self.$('input[name=\"rating_value\"]');\n            self.$star_list = self.$('.o-mail-Composer-stars').find('i');\n            // if this is the first review, we do not use grey color contrast, even with default rating value.\n            if (!self.options.default_message_id) {\n                self.$star_list.removeClass('text-black-25');\n            }\n\n            // set the default value to trigger the display of star widget and update the hidden input value.\n            self._updateStarValue(self.options.default_rating_value);\n            self.$input.val(self.options.default_rating_value);\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     * @private\n     */\n    _prepareMessageData: function () {\n        const options = this._super(...arguments);\n        return Object.assign(options || {}, {\n            message_id: this.options.default_message_id,\n            post_data: { ...options.post_data, rating_value: this.$input.val() },\n        });\n    },\n    /**\n     * @private\n     */\n    _updateStarValue: function (val) {\n        this._starValue = val;\n        var index = Math.floor(val);\n        var decimal = val - index;\n        // reset the stars\n        this.$star_list.removeClass('fa-star fa-star-half-o').addClass('fa-star-o');\n\n        this.$('.o-mail-Composer-stars').find(\"i:lt(\" + index + \")\").removeClass('fa-star-o fa-star-half-o').addClass('fa-star');\n        if (decimal) {\n            this.$('.o-mail-Composer-stars').find(\"i:eq(\" + index + \")\").removeClass('fa-star-o fa-star fa-star-half-o').addClass('fa-star-half-o');\n        }\n    },\n    /**\n     * @private\n     */\n    _onClickStar: function (ev) {\n        var index = this.$('.o-mail-Composer-stars i').index(ev.currentTarget);\n        this._updateStarValue(index + 1);\n        this.user_click = true;\n        this.$input.val(this._starValue);\n    },\n    /**\n     * @private\n     * @param {MouseEvent} ev\n     */\n    _onMoveStar: function (ev) {\n        var index = this.$('.o-mail-Composer-stars i').index(ev.currentTarget);\n        this._updateStarValue(index + 1);\n    },\n    /**\n     * @private\n     */\n    _onMoveLeaveStar: function () {\n        if (!this.user_click) {\n            this._updateStarValue(parseInt(this.$input.val()));\n        }\n        this.user_click = false;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     * @private\n     */\n    _onSubmitButtonClick: function (ev) {\n        return this._super(...arguments).then((result) => {\n            const $modal = this.$el.closest('#ratingpopupcomposer');\n            $modal.on('hidden.bs.modal', () => {\n              this.trigger_up('reload_rating_popup_composer', result);\n            });\n            $modal.modal('hide');\n        }, () => {});\n    },\n\n    /**\n     * @override\n     * @private\n     */\n    _onSubmitCheckContent: function (ev) {\n        if (this.options.rate_with_void_content) {\n            if (this.$input.val() === 0) {\n                return _t('The rating is required. Please make sure to select one before sending your review.')\n            }\n            return false;\n        }\n        return this._super.apply(this, arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport portalComposer from \"@portal/js/portal_composer\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { user } from \"@web/core/user\";\n\nconst PortalComposer = portalComposer.PortalComposer;\n\n/**\n * RatingPopupComposer\n *\n * Display the rating average with a static star widget, and open\n * a popup with the portal composer when clicking on it.\n **/\nconst RatingPopupComposer = publicWidget.Widget.extend({\n    selector: '.o_rating_popup_composer',\n    custom_events: {\n        reload_rating_popup_composer: '_onReloadRatingPopupComposer',\n    },\n\n    willStart: function (parent) {\n        const def = this._super.apply(this, arguments);\n\n        const options = this.$el.data();\n        this.rating_avg = Math.round(options['rating_avg'] * 100) / 100 || 0.0;\n        this.rating_count = options['rating_count'] || 0.0;\n\n        this.options = Object.assign({\n            'token': false,\n            'res_model': false,\n            'res_id': false,\n            'pid': 0,\n            'display_rating': true,\n            'csrf_token': odoo.csrf_token,\n            'user_id': user.userId,\n        }, options, {});\n        this.options.send_button_label = this.options.default_message_id ? _t(\"Update review\") : _t(\"Post review\");\n\n        return def;\n    },\n\n    /**\n     * @override\n     */\n    start: function () {\n        return Promise.all([\n            this._super.apply(this, arguments),\n            this._reloadRatingPopupComposer(),\n        ]);\n    },\n\n    /**\n     * Destroy existing ratingPopup and insert new ratingPopup widget\n     *\n     * @private\n     * @param {Object} data\n     */\n    _reloadRatingPopupComposer: function () {\n        if (this.options.hide_rating_avg) {\n            this.$('.o_rating_popup_composer_stars').empty();\n        } else {\n            const ratingAverage = renderToElement(\n                'portal_rating.rating_stars_static', {\n                inline_mode: true,\n                widget: this,\n                val: this.rating_avg,\n            });\n            this.$('.o_rating_popup_composer_stars').empty().html(ratingAverage);\n        }\n\n        // Append the modal\n        const modal = renderToElement(\n            'portal_rating.PopupComposer', {\n            inline_mode: true,\n            widget: this,\n            val: this.rating_avg,\n        }) || '';\n        this.$('.o_rating_popup_composer_modal').html(modal);\n\n        if (this._composer) {\n            this._composer.destroy();\n        }\n\n        // Instantiate the \"Portal Composer\" widget and insert it into the modal\n        this._composer = new PortalComposer(this, this.options);\n        return this._composer.appendTo(this.$('.o_rating_popup_composer_modal .o_portal_chatter_composer')).then(() => {\n            // Change the text of the button\n            this.$('.o_rating_popup_composer_text').text(\n                this.options.is_fullscreen ?\n                _t('Review') : this.options.default_message_id ?\n                _t('Edit Review') : _t('Add Review')\n            );\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {OdooEvent} event\n     */\n    _onReloadRatingPopupComposer: function (event) {\n        const data = event.data;\n\n        // Refresh the internal state of the widget\n        this.rating_avg = data.rating_avg || data[\"mail.thread\"][0].rating_avg;\n        this.rating_count = data.rating_count || data[\"mail.thread\"][0].rating_count;\n        this.rating_value = data.rating_value || data[\"rating.rating\"]?.[0].rating;\n\n        // Clean the dictionary\n        delete data.rating_avg;\n        delete data.rating_count;\n        delete data.rating_value;\n\n        this._update_options(data);\n        this._reloadRatingPopupComposer();\n    },\n\n    _update_options: function (data) {\n        const defaultOptions = {\n            default_message:\n                data.default_message ||\n                (data[\"mail.message\"] && data[\"mail.message\"][0].body.replace(/<[^>]+>/g, \"\")),\n            default_message_id: data.default_message_id || data[\"mail.message\"][0].id,\n            default_attachment_ids: data.default_attachment_ids || data[\"ir.attachment\"],\n            default_rating_value: data.default_rating_value || this.rating_value,\n        };\n        Object.assign(data, defaultOptions);\n        this.options = Object.assign(this.options, data);\n    },\n});\n\npublicWidget.registry.RatingPopupComposer = RatingPopupComposer;\n\nexport default RatingPopupComposer;\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { clickOnElement } from '@website/js/tours/tour_utils';\n\nexport function addToCart({productName, search = true, productHasVariants = false}) {\n    const steps = [];\n    if (search) {\n        steps.push(...searchProduct(productName));\n    }\n    steps.push(clickOnElement(productName, `a:contains(${productName})`));\n    steps.push(clickOnElement('Add to cart', '#add_to_cart'));\n    if (productHasVariants) {\n        steps.push(clickOnElement('Continue Shopping', 'button:contains(\"Continue Shopping\")'));\n    }\n    return steps;\n}\n\nexport function assertCartAmounts({taxes = false, untaxed = false, total = false, delivery = false}) {\n    let steps = [];\n    if (taxes) {\n        steps.push({\n            content: 'Check if the tax is correct',\n            trigger: `tr#order_total_taxes .oe_currency_value:contains(/^${taxes}$/)`,\n        });\n    }\n    if (untaxed) {\n        steps.push({\n            content: 'Check if the tax is correct',\n            trigger: `tr#order_total_untaxed .oe_currency_value:contains(/^${untaxed}$/)`,\n        });\n    }\n    if (total) {\n        steps.push({\n            content: 'Check if the tax is correct',\n            trigger: `tr#order_total .oe_currency_value:contains(/^${total}$/)`,\n        });\n    }\n    if (delivery) {\n        steps.push({\n            content: 'Check if the tax is correct',\n            trigger: `tr#order_delivery .oe_currency_value:contains(/^${delivery}$/)`,\n        });\n    }\n    return steps\n}\n\nexport function assertCartContains({productName, backend, notContains = false} = {}) {\n    let trigger = `a:contains(${productName})`;\n\n    if (notContains) {\n        trigger = `:not(${trigger})`;\n    }\n    return {\n        content: `Checking if ${productName} is in the cart`,\n        trigger: `${backend ? \":iframe\" : \"\"} ${trigger}`,\n    };\n}\n\n/**\n * Used to assert if the price attribute of a given product is correct on the /shop view\n */\nexport function assertProductPrice(attribute, value, productName) {\n    return {\n        content: `The ${attribute} of the ${productName} is ${value}`,\n        trigger: `div:contains(\"${productName}\") [data-oe-expression=\"template_price_vals['${attribute}']\"] .oe_currency_value:contains(\"${value}\")`,\n    };\n}\n\nexport function fillAdressForm(adressParams = {\n    name: \"John Doe\",\n    phone: \"123456789\",\n    email: \"johndoe@gmail.com\",\n    street: \"1 rue de la paix\",\n    city: \"Paris\",\n    zip: \"75000\"\n}) {\n    let steps = [];\n    steps.push({\n        content: \"Address filling\",\n        trigger: 'form.checkout_autoformat',\n        run() {\n            document.querySelector('input[name=\"name\"]').value = adressParams.name;\n            document.querySelector('input[name=\"phone\"]').value = adressParams.phone;\n            document.querySelector('input[name=\"email\"]').value = adressParams.email;\n            document.querySelector('input[name=\"street\"]').value = adressParams.street;\n            document.querySelector('input[name=\"city\"]').value = adressParams.city;\n            document.querySelector('input[name=\"zip\"]').value = adressParams.zip;\n            document.querySelectorAll(\"#o_country_id option\")[1].selected = true;\n        }\n    });\n    steps.push({\n        content: \"Continue checkout\",\n        trigger: '#save_address',\n        run: 'click',\n    });\n    return steps;\n}\n\nexport function goToCart({quantity = 1, position = \"bottom\", backend = false} = {}) {\n    return {\n        content: _t(\"Go to cart\"),\n        trigger: `${backend ? \":iframe\" : \"\"} a sup.my_cart_quantity:contains(/^${quantity}$/)`,\n        tooltipPosition: position,\n        run: \"click\",\n    };\n}\n\nexport function goToCheckout() {\n    return {\n        content: 'Checkout your order',\n        trigger: 'a[href^=\"/shop/checkout\"]',\n        run: 'click',\n    };\n}\n\nexport function confirmOrder() {\n    return {\n        content: 'Confirm',\n        trigger: 'a[href^=\"/shop/confirm_order\"]',\n        run: 'click',\n    };\n}\n\nexport function pay() {\n    return {\n        content: 'Pay',\n        //Either there are multiple payment methods, and one is checked, either there is only one, and therefore there are no radio inputs\n        trigger: 'button[name=\"o_payment_submit_button\"]:visible:not(:disabled)',\n        run: \"click\",\n    };\n}\n\nexport function payWithDemo() {\n    return [{\n        content: 'eCommerce: select Test payment provider',\n        trigger: 'input[name=\"o_payment_radio\"][data-payment-method-code=\"demo\"]',\n        run: \"click\",\n    }, {\n        content: 'eCommerce: add card number',\n        trigger: 'input[name=\"customer_input\"]',\n        run: \"edit 4242424242424242\",\n    },\n    pay(),\n    {\n        content: 'eCommerce: check that the payment is successful',\n        trigger: '.oe_website_sale_tx_status:contains(\"Your payment has been successfully processed.\")',\n    }]\n}\n\nexport function payWithTransfer(redirect=false) {\n    const first_step = {\n        content: \"Select `Wire Transfer` payment method\",\n        trigger: 'input[name=\"o_payment_radio\"][data-payment-method-code=\"wire_transfer\"]',\n        run: \"click\",\n    }\n    if (!redirect) {\n        return [\n        first_step,\n        pay(),\n        {\n            content: \"Last step\",\n            trigger: '.oe_website_sale_tx_status:contains(\"Please use the following transfer details\")',\n            timeout: 30000,\n        }]\n    } else {\n        return [\n            first_step,\n            pay(),\n            {\n                content: \"Last step\",\n                trigger: '.oe_website_sale_tx_status:contains(\"Please use the following transfer details\")',\n                timeout: 30000,\n                run() {\n                    window.location.href = '/contactus'; // Redirect in JS to avoid the RPC loop (20x1sec)\n                },\n            }, {\n                content: \"wait page loaded\",\n                trigger: 'h1:contains(\"Contact us\")',\n            }\n        ]\n    }\n}\n\nexport function searchProduct(productName) {\n    return [\n        clickOnElement('Shop', 'a:contains(\"Shop\")'),\n        {\n            content: \"Search for the product\",\n            trigger: 'form input[name=\"search\"]',\n            run: `edit ${productName}`,\n        },\n        clickOnElement('Search', 'form:has(input[name=\"search\"]) .oe_search_button'),\n    ];\n}\n\n/**\n * Used to select a pricelist on the /shop view\n */\nexport function selectPriceList(pricelist) {\n    return [\n        {\n            content: \"Click on pricelist dropdown\",\n            trigger: \"div.o_pricelist_dropdown a[data-bs-toggle=dropdown]\",\n            run: \"click\",\n        },\n        {\n            content: \"Click on pricelist\",\n            trigger: `span:contains(${pricelist})`,\n            run: \"click\",\n        },\n    ];\n}\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { debounce } from \"@web/core/utils/timing\";\n\npublicWidget.registry.websiteSaleAddress = publicWidget.Widget.extend({\n    // /shop/address\n    selector: '.o_wsale_address_fill',\n    events: {\n        'change select[name=\"country_id\"]': '_onChangeCountry',\n        'click #save_address': '_onSaveAddress',\n        \"change select[name='state_id']\": \"_onChangeState\",\n    },\n\n    /**\n     * @constructor\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n\n        this.http = this.bindService('http');\n\n        this._changeCountry = debounce(this._changeCountry.bind(this), 500);\n        this.addressForm = document.querySelector('form.checkout_autoformat');\n        this.errorsDiv = document.getElementById('errors');\n        this.addressType = this.addressForm['address_type'].value;\n        this.countryCode = this.addressForm.dataset.companyCountryCode;\n        this.requiredFields = this.addressForm.required_fields.value.split(',');\n    },\n\n    /**\n     * @override\n     */\n    start() {\n        const def = this._super(...arguments);\n\n        this.requiredFields.forEach((fname) => {\n            this._markRequired(fname, true);\n        })\n        this._changeCountry(true);\n\n        return def;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onChangeCountry(ev) {\n        return this._changeCountry();\n    },\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onChangeState(ev) {\n        return Promise.resolve();\n    },\n\n    /**\n     * @private\n     */\n    async _changeCountry(init=false) {\n        const countryId = parseInt(this.addressForm.country_id.value);\n        if (!countryId) {\n            return;\n        }\n\n        const data = await rpc(\n            `/shop/country_info/${parseInt(countryId)}`,\n            {address_type: this.addressType},\n        );\n\n        if (data.phone_code !== 0) {\n            this.addressForm.phone.placeholder = '+' + data.phone_code;\n        } else {\n            this.addressForm.phone.placeholder = '';\n        }\n\n        // populate states and display\n        var selectStates = this.addressForm.state_id;\n        if (!init || selectStates.options.length === 1) {\n            // dont reload state at first loading (done in qweb)\n            if (data.states.length || data.state_required) {\n                // empty existing options, only keep the placeholder.\n                selectStates.options.length = 1;\n\n                // create new options and append them to the select element\n                data.states.forEach((state) => {\n                    let option = new Option(state[1], state[0]);\n                    // Used by localizations\n                    option.setAttribute('data-code', state[2]);\n                    selectStates.appendChild(option);\n                });\n                this._showInput('state_id');\n            } else {\n                this._hideInput('state_id');\n            }\n        }\n\n        // manage fields order / visibility\n        if (data.fields) {\n            if (data.zip_before_city) {\n                this._getInputDiv('zip').after(this._getInputDiv('city'));\n            } else {\n                this._getInputDiv('zip').before(this._getInputDiv('city'));\n            }\n\n            var all_fields = ['street', 'zip', 'city'];\n            all_fields.forEach((fname) => {\n                if (data.fields.includes(fname)) {\n                    this._showInput(fname);\n                } else {\n                    this._hideInput(fname);\n                }\n            });\n        }\n\n        const required_fields = this.addressForm.querySelectorAll(':required');\n        required_fields.forEach((element) => {\n            // remove requirement on previously required fields\n            if (\n                !data.required_fields.includes(element.name)\n                && !this.requiredFields.includes(element.name)\n            ) {\n                this._markRequired(element.name, false);\n            }\n        });\n        data.required_fields.forEach((fieldName) => {\n            this._markRequired(fieldName, true);\n        })\n    },\n\n    _getInputDiv(name) {\n        return this.addressForm[name].parentElement;\n    },\n\n    _getInputLabel(name) {\n        const input = this.addressForm[name];\n        return input?.parentElement.querySelector(`label[for='${input.id}']`);\n    },\n\n    _showInput(name) {\n        // show parent div, containing label and input\n        this.addressForm[name].parentElement.style.display = '';\n    },\n\n    _hideInput(name) {\n        // show parent div, containing label and input\n        this.addressForm[name].parentElement.style.display = 'none';\n    },\n\n    _markRequired(name, required) {\n        const input = this.addressForm[name];\n        if (input) {\n            input.required = required;\n        }\n        this._getInputLabel(name)?.classList.toggle('label-optional', !required);\n    },\n\n    /**\n     * Disable the button, submit the form and add a spinner while the submission is ongoing\n     *\n     * @private\n     * @param {Event} ev\n     */\n    async _onSaveAddress(ev) {\n        if (!this.addressForm.reportValidity()) {\n            return\n        }\n\n        const submitButton = ev.currentTarget;\n        if (!ev.defaultPrevented && !submitButton.disabled) {\n            ev.preventDefault();\n\n            submitButton.disabled = true;\n            const spinner = document.createElement('span');\n            spinner.classList.add('fa', 'fa-cog', 'fa-spin');\n            submitButton.appendChild(spinner);\n\n            const result = await this.http.post(\n                '/shop/address/submit',\n                new FormData(this.addressForm),\n            )\n            if (result.successUrl) {\n                window.location = result.successUrl;\n            } else {\n                // Highlight missing/invalid form values\n                document.querySelectorAll('.is-invalid').forEach(element => {\n                    if (!result.invalid_fields.includes(element.name)) {\n                        element.classList.remove('is-invalid');\n                    }\n                })\n                result.invalid_fields.forEach(\n                    fieldName => this.addressForm[fieldName].classList.add('is-invalid')\n                );\n\n                // Display the error messages\n                // NOTE: setCustomValidity is not used as we would have to reset the error msg on\n                // input update, which is not worth catching for the rare cases where the\n                // server-side validation will catch validation issues (now that required inputs\n                // are also handled client-side)\n                const newErrors = result.messages.map(message => {\n                    const errorHeader = document.createElement('h5');\n                    errorHeader.classList.add('text-danger');\n                    errorHeader.appendChild(document.createTextNode(message));\n                    return errorHeader;\n                });\n\n                this.errorsDiv.replaceChildren(...newErrors);\n\n                // Re-enable button and remove spinner\n                submitButton.disabled = false;\n                spinner.remove();\n            }\n        }\n    },\n\n});\n\nexport default publicWidget.registry.websiteSaleAddress;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\npublicWidget.registry.websiteSaleCart = publicWidget.Widget.extend({\n    selector: '.oe_website_sale .oe_cart',\n    events: {\n        'click .js_delete_product': '_onClickDeleteProduct',\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onClickDeleteProduct: function (ev) {\n        ev.preventDefault();\n        $(ev.currentTarget).closest('.o_cart_product').find('.js_quantity').val(0).trigger('change');\n    },\n});\n\nexport default publicWidget.registry.websiteSaleCart;\n", "import {\n    LocationSelectorDialog\n} from '@delivery/js/location_selector/location_selector_dialog/location_selector_dialog';\nimport { _t } from '@web/core/l10n/translation';\nimport { rpc } from '@web/core/network/rpc';\nimport publicWidget from '@web/legacy/js/public/public_widget';\n\npublicWidget.registry.WebsiteSaleCheckout = publicWidget.Widget.extend({\n    selector: '#shop_checkout',\n    events: {\n        // Addresses\n        'click .card': '_changeAddress',\n        'click .js_edit_address': '_preventChangingAddress',\n        'change #use_delivery_as_billing': '_toggleBillingAddressRow',\n        // Delivery methods\n        'click [name=\"o_delivery_radio\"]': '_selectDeliveryMethod',\n        'click [name=\"o_pickup_location_selector\"]': '_selectPickupLocation',\n    },\n\n    // #=== WIDGET LIFECYCLE ===#\n\n    async start() {\n        this.mainButton = document.querySelector('a[name=\"website_sale_main_button\"]');\n        this.use_delivery_as_billing_toggle = document.querySelector('#use_delivery_as_billing');\n        this.billingContainer = this.el.querySelector('#billing_container');\n        await this._prepareDeliveryMethods();\n    },\n\n    // #=== EVENT HANDLERS ===#\n\n    /**\n     * Set the billing or delivery address on the order and update the corresponding card.\n     *\n     * @private\n     * @param {Event} ev\n     * @return {void}\n     */\n    async _changeAddress(ev) {\n        const newAddress = ev.currentTarget;\n        if (newAddress.classList.contains('bg-primary')) { // If the card is already selected.\n            return;\n        }\n        const addressType = newAddress.dataset.addressType;\n\n        // Remove the highlighting from the previously selected address card.\n        const previousAddress = this._getSelectedAddress(addressType);\n        this._tuneDownAddressCard(previousAddress);\n\n        // Highlight the newly selected address card.\n        this._highlightAddressCard(newAddress);\n        const selectedPartnerId = newAddress.dataset.partnerId;\n        await this.updateAddress(addressType, selectedPartnerId);\n        if (addressType === 'delivery') {  // A delivery address is changed.\n            if (this.use_delivery_as_billing_toggle.checked) {\n                await this._selectMatchingBillingAddress(selectedPartnerId);\n            }\n            // Update the available delivery methods.\n            document.getElementById('o_delivery_form').innerHTML = await rpc(\n                '/shop/delivery_methods'\n            );\n            await this._prepareDeliveryMethods();\n        }\n        this._enableMainButton();  // Try to enable the main button.\n    },\n\n    /**\n     * Show/hide the billing address row when the user toggles the 'use delivery as billing' input.\n     *\n     * The URLs of the \"create address\" buttons are updated to propagate the value of the input.\n     *\n     * @private\n     * @param ev\n     * @return {void}\n     */\n    async _toggleBillingAddressRow(ev) {\n        const useDeliveryAsBilling = ev.target.checked;\n\n        const addDeliveryAddressButton = this.el.querySelector(\n            '.o_wsale_add_address[data-address-type=\"delivery\"]'\n        );\n        if (addDeliveryAddressButton) {  // If `Add address` button for delivery.\n            // Update the `use_delivery_as_billing` query param for a new delivery address URL.\n            const addDeliveryUrl = new URL(addDeliveryAddressButton.href);\n            addDeliveryUrl.searchParams.set(\n                'use_delivery_as_billing', encodeURIComponent(useDeliveryAsBilling)\n            );\n            addDeliveryAddressButton.href = addDeliveryUrl.toString();\n        }\n\n        // Toggle the billing address row.\n        if (useDeliveryAsBilling) {\n            this.billingContainer.classList.add('d-none');  // Hide the billing address row.\n            const selectedDeliveryAddress = this._getSelectedAddress('delivery');\n            await this._selectMatchingBillingAddress(selectedDeliveryAddress.dataset.partnerId);\n        } else {\n            this._disableMainButton();\n            this.billingContainer.classList.remove('d-none');  // Show the billing address row.\n        }\n\n        this._enableMainButton();  // Try to enable the main button.\n    },\n\n    /**\n     * Cancel the address change to allow the redirect to the edit page to take place.\n     *\n     * @private\n     * @param {Event} ev\n     * @return {void}\n     */\n    _preventChangingAddress(ev) {\n        ev.stopPropagation();\n    },\n\n    /**\n     * Fetch the delivery rate for the selected delivery method and update the displayed amounts.\n     *\n     * @private\n     * @param {Event} ev\n     * @return {void}\n     */\n    async _selectDeliveryMethod(ev) {\n        const checkedRadio = ev.currentTarget;\n        if (checkedRadio.disabled) {  // The delivery rate request failed.\n            return; // Failing delivery methods cannot be selected.\n        }\n\n        // Disable the main button while fetching delivery rates.\n        this._disableMainButton();\n\n        // Hide and reset the order location name and address if defined.\n        this._hidePickupLocation();\n\n        // Fetch delivery rates and update the cart summary and the price badge accordingly.\n        await this._updateDeliveryMethod(checkedRadio);\n\n        // Re-enable the main button after delivery rates have been fetched.\n        this._enableMainButton();\n\n        // Show a button to open the location selector if required for the selected delivery method.\n        await this._showPickupLocation(checkedRadio);\n    },\n\n    /**\n     * Fetch and display the closest pickup locations based on the zip code.\n     *\n     * @private\n     * @param {Event} ev\n     * @return {void}\n     */\n    async _selectPickupLocation(ev) {\n        const { zipCode, locationId } = ev.currentTarget.dataset;\n        const deliveryMethodContainer = this._getDeliveryMethodContainer(ev.currentTarget);\n        this.call('dialog', 'add', LocationSelectorDialog, {\n            zipCode: zipCode,\n            selectedLocationId: locationId,\n            isFrontend: true,\n            save: async location => {\n                const jsonLocation = JSON.stringify(location);\n                // Assign the selected pickup location to the order.\n                await this._setPickupLocation(jsonLocation);\n\n                //  Show and set the order location details.\n                this._updatePickupLocation(deliveryMethodContainer, location, jsonLocation);\n\n                this._enableMainButton();\n            },\n        });\n    },\n\n    // #=== DOM MANIPULATION ===#\n\n    /**\n     * Update the pickup location address elements and the 'edit' button's values.\n     *\n     * @private\n     * @param deliveryMethodContainer - The container element of the delivery method.\n     * @param location - The selected location as an object.\n     * @param jsonLocation - The selected location as an JSON string.\n     * @return {void}\n     */\n    _updatePickupLocation(deliveryMethodContainer, location, jsonLocation) {\n        const pickupLocation = deliveryMethodContainer.querySelector('[name=\"o_pickup_location\"]');\n        pickupLocation.querySelector('[name=\"o_pickup_location_name\"]').innerText = location.name;\n        pickupLocation.querySelector(\n            '[name=\"o_pickup_location_address\"]'\n        ).innerText = `${location.street} ${location.zip_code} ${location.city}`;\n        const editPickupLocationButton = pickupLocation.querySelector(\n            'span[name=\"o_pickup_location_selector\"]'\n        );\n        editPickupLocationButton.dataset.locationId = location.id;\n        editPickupLocationButton.dataset.zipCode = location.zip_code;\n        editPickupLocationButton.dataset.pickupLocationData = jsonLocation;\n        pickupLocation.querySelector(\n            '[name=\"o_pickup_location_details\"]'\n        ).classList.remove('d-none');\n\n        // Remove the button.\n        pickupLocation.querySelector('button[name=\"o_pickup_location_selector\"]')?.remove();\n    },\n\n    /**\n     * Remove the highlighting from the address card.\n     *\n     * @private\n     * @param card - The card element of the selected address.\n     * @return {void}\n     */\n    _tuneDownAddressCard(card) {\n        if (!card) return;\n        card.classList.remove('bg-primary', 'border', 'border-primary');\n    },\n\n    /**\n     * Highlight the address card.\n     *\n     * @private\n     * @param card - The card element of the selected address.\n     * @return {void}\n     */\n    _highlightAddressCard(card) {\n        if (!card) return;\n        card.classList.add('bg-primary', 'border', 'border-primary');\n    },\n\n    /**\n     * Disable the main button.\n     *\n     * @private\n     * @return {void}\n     */\n    _disableMainButton() {\n        this.mainButton?.classList.add('disabled');\n    },\n\n    /**\n     * Enable the main button if all conditions are satisfied.\n     *\n     * @private\n     * @return {void}\n     */\n    _enableMainButton() {\n        if (this._canEnableMainButton()) {\n            this.mainButton?.classList.remove('disabled');\n        }\n    },\n\n    /**\n     * Return whether a delivery method and a billing address are selected.\n     *\n     * @private\n     * @return {boolean}\n     */\n    _canEnableMainButton(){\n        return this._isDeliveryMethodReady() && this._isBillingAddressSelected();\n    },\n\n    /**\n     * Hide the pickup location.\n     *\n     * @private\n     * @return {void}\n     */\n    _hidePickupLocation() {\n        const pickupLocations = document.querySelectorAll(\n            '[name=\"o_pickup_location\"]:not(.d-none)'\n        );\n        pickupLocations.forEach(pickupLocation => {\n            pickupLocation.classList.add('d-none'); // Hide the whole div.\n        });\n    },\n\n    /**\n     * Set the delivery method on the order and update the price badge and cart summary.\n     *\n     * @private\n     * @param {HTMLInputElement} radio - The radio button linked to the delivery method.\n     * @return {void}\n     */\n    async _updateDeliveryMethod(radio) {\n        this._showLoadingBadge(radio);\n        const result = await this._setDeliveryMethod(radio.dataset.dmId);\n        this._updateAmountBadge(radio, result);\n        this._updateCartSummary(result);\n    },\n\n    /**\n     * Display a loading spinner on the delivery price badge.\n     *\n     * @private\n     * @param {HTMLInputElement} radio - The radio button linked to the delivery method.\n     * @return {void}\n     */\n    _showLoadingBadge(radio) {\n        const deliveryPriceBadge = this._getDeliveryPriceBadge(radio);\n        this._clearElement(deliveryPriceBadge);\n        deliveryPriceBadge.appendChild(this._createLoadingElement());\n    },\n\n    /**\n     * Update the delivery price badge with the delivery rate.\n     *\n     * If the rate is zero, the price badge displays \"Free\" instead.\n     *\n     * @private\n     * @param {HTMLInputElement} radio - The radio button linked to the delivery method.\n     * @param {Object} rateData - The delivery rate data.\n     * @return {void}\n     */\n    _updateAmountBadge(radio, rateData) {\n        const deliveryPriceBadge = this._getDeliveryPriceBadge(radio);\n        if (rateData.success) {\n             // If it's a free delivery (`free_over` field), show 'Free', not '$ 0'.\n             if (rateData.is_free_delivery) {\n                 deliveryPriceBadge.textContent = _t(\"Free\");\n             } else {\n                 deliveryPriceBadge.innerHTML = rateData.amount_delivery;\n             }\n             this._toggleDeliveryMethodRadio(radio);\n        } else {\n            deliveryPriceBadge.textContent = rateData.error_message;\n            this._toggleDeliveryMethodRadio(radio, true);\n        }\n    },\n\n    /**\n     * Update the order summary table with the delivery rate of the selected delivery method.\n     *\n     * @private\n     * @param {Object} result - The order summary values.\n     * @return {void}\n     */\n    _updateCartSummary(result) {\n        const amountDelivery = document.querySelector('#order_delivery .monetary_field');\n        const amountUntaxed = document.querySelector('#order_total_untaxed .monetary_field');\n        const amountTax = document.querySelector('#order_total_taxes .monetary_field');\n        const amountTotal = document.querySelectorAll(\n            '#order_total .monetary_field, #amount_total_summary.monetary_field'\n        );\n        amountDelivery.innerHTML = result.amount_delivery;\n        amountUntaxed.innerHTML = result.amount_untaxed;\n        amountTax.innerHTML = result.amount_tax;\n        amountTotal.forEach(total => total.innerHTML = result.amount_total);\n    },\n\n    /**\n     * Enable or disable radio selection for a delivery method.\n     *\n     * @private\n     * @param {HTMLInputElement} radio - The radio button linked to the delivery method.\n     * @param {Boolean} disable - Whether the radio should be disabled.\n     */\n    _toggleDeliveryMethodRadio(radio, disable=false) {\n        const deliveryMethodContainer = this._getDeliveryMethodContainer(radio);\n        radio.disabled = disable;\n        if (disable) {\n            deliveryMethodContainer.classList.add('text-muted');\n        }\n        else {\n            deliveryMethodContainer.classList.remove('text-muted');\n        }\n    },\n\n    /**\n     * Remove all children of the provided element from the DOM.\n     *\n     * @private\n     * @param {Element} el - The element to clear.\n     * @return {void}\n     */\n    _clearElement(el) {\n        while (el.firstChild) {\n            el.removeChild(el.lastChild);\n        }\n    },\n\n    // #=== ADDRESS FLOW ===#\n\n    /**\n     * Select the billing address matching the currently selected delivery address.\n     *\n     * @private\n     * @param selectedPartnerId - The partner id of the selected delivery address.\n     * @return {void}\n     */\n    async _selectMatchingBillingAddress(selectedPartnerId) {\n        const previousAddress = this._getSelectedAddress('billing');\n        this._tuneDownAddressCard(previousAddress);\n        await this.updateAddress('billing', selectedPartnerId);\n        const billingAddress = this.el.querySelector(\n            `.card[data-partner-id=\"${selectedPartnerId}\"][data-address-type=\"billing\"]`\n        );\n        this._highlightAddressCard(billingAddress);\n    },\n\n    /**\n     * Set the billing or delivery address on the order.\n     *\n     * @param addressType - The type of the address to set: 'delivery' or 'billing'.\n     * @param partnerId - The partner id of the address to set.\n     * @return {void}\n     */\n    async updateAddress(addressType, partnerId) {\n        await rpc('/shop/update_address', {address_type: addressType, partner_id: partnerId})\n    },\n\n    // #=== DELIVERY FLOW ===#\n\n    /**\n     * Change the delivery method to the one whose radio is selected and fetch all delivery rates.\n     *\n     * @private\n     * @return {void}\n     */\n    async _prepareDeliveryMethods() {\n        // Load the radios from the DOM here to update them if the template is re-rendered.\n        this.dmRadios = Array.from(document.querySelectorAll('input[name=\"o_delivery_radio\"]'));\n        if (this.dmRadios.length > 0) {\n            const checkedRadio = document.querySelector('input[name=\"o_delivery_radio\"]:checked');\n            this._disableMainButton();\n            if (checkedRadio) {\n                await this._updateDeliveryMethod(checkedRadio);\n                this._enableMainButton();\n            }\n        }\n        // Asynchronously fetch delivery rates to mitigate delays from third-party APIs\n        await Promise.all(this.dmRadios.filter(radio => !radio.checked).map(async radio => {\n            this._showLoadingBadge((radio));\n            const rateData = await this._getDeliveryRate(radio);\n            this._updateAmountBadge(radio, rateData);\n        }));\n    },\n\n    /**\n     * Check if the delivery method is selected and if the pickup point is selected if needed.\n     *\n     * @private\n     * @return {boolean} Whether the delivery method is ready.\n     */\n    _isDeliveryMethodReady() {\n        if (this.dmRadios.length === 0) { // No delivery method is available.\n            return true; // Ignore the check.\n        }\n        const checkedRadio = document.querySelector('input[name=\"o_delivery_radio\"]:checked');\n        return checkedRadio\n            && !checkedRadio.disabled\n            && !this._isPickupLocationMissing(checkedRadio);\n    },\n\n    /**\n     * Get the delivery rate of the delivery method linked to the provided radio.\n     *\n     * @private\n     * @param {HTMLInputElement} radio - The radio button linked to the delivery method.\n     * @return {Object} The delivery rate data.\n     */\n    async _getDeliveryRate(radio) {\n        return await rpc('/shop/get_delivery_rate', {'dm_id': radio.dataset.dmId});\n    },\n\n    /**\n     * Set the delivery method on the order and return the result values.\n     *\n     * @private\n     * @param {Integer} dmId - The id of selected delivery method.\n     * @return {Object} The result values.\n     */\n    async _setDeliveryMethod(dmId) {\n        return await rpc('/shop/set_delivery_method', {'dm_id': dmId});\n    },\n\n    /**\n     * Show the pickup location information or the button to open the location selector.\n     *\n     * @private\n     * @param {HTMLInputElement} radio - The radio button linked to the delivery method.\n     * @return {void}\n     */\n    async _showPickupLocation(radio) {\n        if (!radio.dataset.isPickupLocationRequired || radio.disabled) {\n            return;  // Fetching the delivery rate failed.\n        }\n        const deliveryMethodContainer = this._getDeliveryMethodContainer(radio);\n        const pickupLocation = deliveryMethodContainer.querySelector('[name=\"o_pickup_location\"]');\n\n        const editPickupLocationButton = pickupLocation.querySelector(\n            'span[name=\"o_pickup_location_selector\"]'\n        );\n        if (editPickupLocationButton.dataset.pickupLocationData) {\n            await this._setPickupLocation(editPickupLocationButton.dataset.pickupLocationData);\n        }\n\n        pickupLocation.classList.remove('d-none'); // Show the whole div.\n    },\n\n    /**\n     * Set the pickup location on the order.\n     *\n     * @private\n     * @param {String} pickupLocationData - The pickup location's data to set.\n     * @return {void}\n     */\n    async _setPickupLocation(pickupLocationData) {\n        await rpc('/website_sale/set_pickup_location', {pickup_location_data: pickupLocationData});\n    },\n\n    // #=== GETTERS & SETTERS ===#\n\n    /** Determine and return the selected address who card has the class rowAddrClass.\n     *\n     * @private\n     * @param addressType - The type of the address: 'billing' or 'delivery'.\n     * @return {Element}\n     */\n    _getSelectedAddress(addressType) {\n        return this.el.querySelector(`.card.bg-primary[data-address-type=\"${addressType}\"]`);\n    },\n\n    /**\n     * Return whether the \"use delivery as billing\" toggle is checked or a billing address is\n     * selected.\n     *\n     * @private\n     * @return {boolean} - Whether a billing address is selected.\n     */\n    _isBillingAddressSelected() {\n        const billingAddressSelected = Boolean(\n            this.el.querySelector('.card.bg-primary[data-address-type=\"billing\"]')\n        );\n        return billingAddressSelected || this.use_delivery_as_billing_toggle.checked;\n    },\n\n    /**\n     * Create and return an element representing a loading spinner.\n     *\n     * @private\n     * @return {Element} The created element.\n     */\n    _createLoadingElement() {\n        const loadingElement = document.createElement('i');\n        loadingElement.classList.add('fa', 'fa-circle-o-notch', 'fa-spin', 'center');\n        return loadingElement;\n    },\n\n    /**\n     * Return the delivery price badge element of the delivery method linked to the provided radio.\n     *\n     * @private\n     * @param {HTMLInputElement} radio - The radio button linked to the delivery method.\n     * @return {Element} The delivery price badge element of the linked delivery method.\n     */\n    _getDeliveryPriceBadge(radio) {\n        const deliveryMethodContainer = this._getDeliveryMethodContainer(radio);\n        return deliveryMethodContainer.querySelector('.o_wsale_delivery_price_badge');\n    },\n\n    /**\n     * Return the container element of the delivery method linked to the provided element.\n     *\n     * @private\n     * @param {Element} el - The element linked to the delivery method.\n     * @return {Element} The container element of the linked delivery method.\n     */\n    _getDeliveryMethodContainer(el) {\n        return el.closest('[name=\"o_delivery_method\"]');\n    },\n\n    /**\n     * Return whether a pickup location is required but not selected.\n     *\n     * @private\n     * @param {HTMLInputElement} radio - The radio button linked to the delivery method.\n     * @return {boolean} Whether a required pickup location is missing.\n     */\n    _isPickupLocationMissing(radio) {\n        const deliveryMethodContainer = this._getDeliveryMethodContainer(radio);\n        if (!this._isPickupLocationRequired(radio)) return false;\n        return !deliveryMethodContainer.querySelector(\n            'span[name=\"o_pickup_location_selector\"]'\n        ).dataset.locationId;\n    },\n\n    /**\n     * Return whether a pickup is required for the delivery method linked to the provided radio.\n     *\n     * @private\n     * @param {HTMLInputElement} radio - The radio button linked to the delivery method.\n     * @return {bool} Whether a pickup is needed.\n     */\n    _isPickupLocationRequired(radio) {\n        return Boolean(radio.dataset.isPickupLocationRequired);\n    },\n\n});\n\nexport default publicWidget.registry.WebsiteSaleCheckout;\n", "/** @odoo-module **/\n\nimport paymentButton from '@payment/js/payment_button';\n\npaymentButton.include({\n\n    /**\n     * Verify that the payment button is ready to be enabled.\n     *\n     * The conditions are that:\n     * - a delivery carrier is selected and ready (the price is computed) if deliveries are enabled;\n     * - the \"Terms and Conditions\" checkbox is ticked if it is present.\n     *\n     * @override from @payment/js/payment_button\n     * @return {boolean}\n     */\n    _canSubmit() {\n        return this._super(...arguments) && this._isTCCheckboxReady();\n    },\n\n    /**\n     * Check if the \"Terms and Conditions\" checkbox is ticked, if present.\n     *\n     * @private\n     * @return {boolean}\n     */\n    _isTCCheckboxReady() {\n        const checkbox = document.querySelector('#website_sale_tc_checkbox');\n        if (!checkbox) { // The checkbox is not present.\n            return true;  // Ignore the check.\n        }\n\n        return checkbox.checked;\n    },\n\n});\n", "/** @odoo-module **/\n\nimport PaymentForm from '@payment/js/payment_form';\n\nPaymentForm.include({\n\n     /**\n      * Create an event listener for the payment button located outside the payment form.\n      * @override\n     */\n     async start() {\n         const submitButton = document.querySelector('[name=\"o_payment_submit_button\"]');\n         submitButton.addEventListener('click', ev => this._submitForm(ev));\n         return await this._super(...arguments);\n     }\n\n});\n", "/** @odoo-module **/\n\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { memoize, uniqueId } from \"@web/core/utils/functions\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { insertThousandsSep } from \"@web/core/utils/numbers\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nvar VariantMixin = {\n    events: {\n        'change .css_attribute_color input': '_onChangeColorAttribute',\n        'click .o_variant_pills': '_onChangePillsAttribute',\n        'change [data-attribute_exclusions]': 'onChangeVariant'\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * When a variant is changed, this will check:\n     * - If the selected combination is available or not\n     * - The extra price if applicable\n     * - The display name of the product (\"Customizable desk (White, Steel)\")\n     * - The new total price\n     * - The need of adding a \"custom value\" input\n     *   If the custom value is the only available value\n     *   (defined by its data 'is_single_and_custom'),\n     *   the custom value will have it's own input & label\n     *\n     * 'change' events triggered by the user entered custom values are ignored since they\n     * are not relevant\n     *\n     * @param {MouseEvent} ev\n     */\n    onChangeVariant: function (ev) {\n        var $parent = $(ev.target).closest('.js_product');\n        if (!$parent.data('uniqueId')) {\n            $parent.data('uniqueId', uniqueId());\n        }\n        this._throttledGetCombinationInfo(this, $parent.data('uniqueId'))(ev);\n    },\n    /**\n     * @see onChangeVariant\n     *\n     * @private\n     * @param {Event} ev\n     * @returns {Deferred}\n     */\n    _getCombinationInfo: function (ev) {\n        if ($(ev.target).hasClass('variant_custom_value')) {\n            return Promise.resolve();\n        }\n\n        const $parent = $(ev.target).closest('.js_product');\n        if(!$parent.length){\n            return Promise.resolve();\n        }\n        const combination = this.getSelectedVariantValues($parent);\n\n        return rpc('/website_sale/get_combination_info', {\n            'product_template_id': parseInt($parent.find('.product_template_id').val()),\n            'product_id': this._getProductId($parent),\n            'combination': combination,\n            'add_qty': parseInt($parent.find('input[name=\"add_qty\"]').val()),\n            'parent_combination': [],\n            'context': this.context,\n            ...this._getOptionalCombinationInfoParam($parent),\n        }).then((combinationData) => {\n            if (this._shouldIgnoreRpcResult()) {\n                return;\n            }\n            this._onChangeCombination(ev, $parent, combinationData);\n            this._checkExclusions($parent, combination, combinationData.parent_exclusions);\n        });\n    },\n\n    /**\n     * Hook to add optional info to the combination info call.\n     *\n     * @param {$.Element} $product\n     */\n    _getOptionalCombinationInfoParam($product) {\n        return {};\n    },\n\n    /**\n     * Will add the \"custom value\" input for this attribute value if\n     * the attribute value is configured as \"custom\" (see product_attribute_value.is_custom)\n     *\n     * @private\n     * @param {MouseEvent} ev\n     */\n    handleCustomValues: function ($target) {\n        var $variantContainer;\n        var $customInput = false;\n        if ($target.is('input[type=radio]') && $target.is(':checked')) {\n            $variantContainer = $target.closest('ul').closest('li');\n            $customInput = $target;\n        } else if ($target.is('select')) {\n            $variantContainer = $target.closest('li');\n            $customInput = $target\n                .find('option[value=\"' + $target.val() + '\"]');\n        }\n\n        if ($variantContainer) {\n            if ($customInput && $customInput.data('is_custom') === 'True') {\n                var attributeValueId = $customInput.data('value_id');\n                var attributeValueName = $customInput.data('value_name');\n\n                if ($variantContainer.find('.variant_custom_value').length === 0\n                        || $variantContainer\n                              .find('.variant_custom_value')\n                              .data('custom_product_template_attribute_value_id') !== parseInt(attributeValueId)) {\n                    $variantContainer.find('.variant_custom_value').remove();\n\n                    const previousCustomValue = $customInput.attr(\"previous_custom_value\");\n                    var $input = $('<input>', {\n                        type: 'text',\n                        'data-custom_product_template_attribute_value_id': attributeValueId,\n                        'data-attribute_value_name': attributeValueName,\n                        class: 'variant_custom_value form-control mt-2'\n                    });\n\n                    $input.attr('placeholder', attributeValueName);\n                    $input.addClass('custom_value_radio');\n                    $variantContainer.append($input);\n                    if (previousCustomValue) {\n                        $input.val(previousCustomValue);\n                    }\n                }\n            } else {\n                $variantContainer.find('.variant_custom_value').remove();\n            }\n        }\n    },\n\n    /**\n     * Hack to add and remove from cart with json\n     *\n     * @param {MouseEvent} ev\n     */\n    onClickAddCartJSON: function (ev) {\n        ev.preventDefault();\n        var $link = $(ev.currentTarget);\n        var $input = $link.closest('.input-group').find(\"input\");\n        var min = parseFloat($input.data(\"min\") || 0);\n        var max = parseFloat($input.data(\"max\") || Infinity);\n        var previousQty = parseFloat($input.val() || 0, 10);\n        var quantity = ($link.has(\".fa-minus\").length ? -1 : 1) + previousQty;\n        var newQty = quantity > min ? (quantity < max ? quantity : max) : min;\n\n        if (newQty !== previousQty) {\n            $input.val(newQty).trigger('change');\n        }\n        return false;\n    },\n\n    /**\n     * When the quantity is changed, we need to query the new price of the product.\n     * Based on the pricelist, the price might change when quantity exceeds a certain amount.\n     *\n     * @param {MouseEvent} ev\n     */\n    onChangeAddQuantity: function (ev) {\n        const $parent = $(ev.currentTarget).closest('form');\n        if ($parent.length > 0) {\n            this.triggerVariantChange($parent);\n        }\n    },\n\n    /**\n     * Triggers the price computation and other variant specific changes\n     *\n     * @param {$.Element} $container\n     */\n    triggerVariantChange: function ($container) {\n        $container.find('ul[data-attribute_exclusions]').trigger('change');\n        $container.find('input.js_variant_change:checked, select.js_variant_change').each(function () {\n            VariantMixin.handleCustomValues($(this));\n        });\n    },\n\n    /**\n     * Will look for user custom attribute values\n     * in the provided container\n     *\n     * @param {$.Element} $container\n     * @returns {Array} array of custom values with the following format\n     *   {integer} custom_product_template_attribute_value_id\n     *   {string} attribute_value_name\n     *   {string} custom_value\n     */\n    getCustomVariantValues: function ($container) {\n        var variantCustomValues = [];\n        $container.find('.variant_custom_value').each(function (){\n            var $variantCustomValueInput = $(this);\n            if ($variantCustomValueInput.length !== 0){\n                variantCustomValues.push({\n                    'custom_product_template_attribute_value_id': $variantCustomValueInput.data('custom_product_template_attribute_value_id'),\n                    'attribute_value_name': $variantCustomValueInput.data('attribute_value_name'),\n                    'custom_value': $variantCustomValueInput.val(),\n                });\n            }\n        });\n\n        return variantCustomValues;\n    },\n\n    /**\n     * Will look for attribute values that do not create product variant\n     * (see product_attribute.create_variant \"dynamic\")\n     *\n     * @param {$.Element} $container\n     * @returns {Array} array of attribute values with the following format\n     *   {integer} custom_product_template_attribute_value_id\n     *   {string} attribute_value_name\n     *   {integer} value\n     *   {string} attribute_name\n     *   {boolean} is_custom\n     */\n    getNoVariantAttributeValues: function ($container) {\n        var noVariantAttributeValues = [];\n        var variantsValuesSelectors = [\n            'input.no_variant.js_variant_change:checked',\n            'select.no_variant.js_variant_change'\n        ];\n\n        $container.find(variantsValuesSelectors.join(',')).each(function (){\n            var $variantValueInput = $(this);\n            var singleNoCustom = $variantValueInput.data('is_single') && !$variantValueInput.data('is_custom');\n\n            if ($variantValueInput.is('select')){\n                $variantValueInput = $variantValueInput.find('option[value=' + $variantValueInput.val() + ']');\n            }\n\n            if ($variantValueInput.length !== 0 && !singleNoCustom){\n                noVariantAttributeValues.push({\n                    'custom_product_template_attribute_value_id': $variantValueInput.data('value_id'),\n                    'attribute_value_name': $variantValueInput.data('value_name'),\n                    'value': $variantValueInput.val(),\n                    'attribute_name': $variantValueInput.data('attribute_name'),\n                    'is_custom': $variantValueInput.data('is_custom')\n                });\n            }\n        });\n\n        return noVariantAttributeValues;\n    },\n\n    /**\n     * Will return the list of selected product.template.attribute.value ids\n     *\n     * @param {$.Element} $container the container to look into\n     */\n    getSelectedVariantValues: function ($container) {\n        var values = [];\n\n        var variantsValuesSelectors = [\n            'input.js_variant_change:checked',\n            'select.js_variant_change'\n        ];\n        $container.find(variantsValuesSelectors.join(', ')).toArray().forEach((el) => {\n            values.push(+$(el).val());\n        });\n\n        return values;\n    },\n\n    /**\n     * Will return a promise:\n     *\n     * - If the product already exists, immediately resolves it with the product_id\n     * - If the product does not exist yet (\"dynamic\" variant creation), this method will\n     *   create the product first and then resolve the promise with the created product's id\n     *\n     * @param {$.Element} $container the container to look into\n     * @param {integer} productId the product id\n     * @param {integer} productTemplateId the corresponding product template id\n     * @returns {Promise} the promise that will be resolved with a {integer} productId\n     */\n    selectOrCreateProduct: function ($container, productId, productTemplateId) {\n        productId = parseInt(productId);\n        productTemplateId = parseInt(productTemplateId);\n        var productReady = Promise.resolve();\n        if (productId) {\n            productReady = Promise.resolve(productId);\n        } else {\n            var params = {\n                product_template_id: productTemplateId,\n                product_template_attribute_value_ids:\n                    JSON.stringify(VariantMixin.getSelectedVariantValues($container)),\n            };\n\n            var route = '/sale/create_product_variant';\n            productReady = rpc(route, params);\n        }\n\n        return productReady;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Will disable attribute value's inputs based on combination exclusions\n     * and will disable the \"add\" button if the selected combination\n     * is not available\n     *\n     * This will check both the exclusions within the product itself and\n     * the exclusions coming from the parent product (meaning that this product\n     * is an option of the parent product)\n     *\n     * It will also check that the selected combination does not exactly\n     * match a manually archived product\n     *\n     * @private\n     * @param {$.Element} $parent the parent container to apply exclusions\n     * @param {Array} combination the selected combination of product attribute values\n     * @param {Array} parentExclusions the exclusions induced by the variant selection of the parent product\n     * For example chair cannot have steel legs if the parent Desk doesn't have steel legs\n     */\n    _checkExclusions: function ($parent, combination, parentExclusions) {\n        var self = this;\n        var combinationData = $parent\n            .find('ul[data-attribute_exclusions]')\n            .data('attribute_exclusions');\n\n        if (parentExclusions && combinationData.parent_exclusions) {\n            combinationData.parent_exclusions = parentExclusions;\n        }\n        $parent\n            .find('option, input, label, .o_variant_pills')\n            .removeClass('css_not_available')\n            .attr('title', function () { return $(this).data('value_name') || ''; })\n            .data('excluded-by', '');\n\n        // exclusion rules: array of ptav\n        // for each of them, contains array with the other ptav they exclude\n        if (combinationData.exclusions) {\n            // browse all the currently selected attributes\n            Object.values(combination).forEach((current_ptav) => {\n                if (combinationData.exclusions.hasOwnProperty(current_ptav)) {\n                    // for each exclusion of the current attribute:\n                    Object.values(combinationData.exclusions[current_ptav]).forEach((excluded_ptav) => {\n                        // disable the excluded input (even when not already selected)\n                        // to give a visual feedback before click\n                        self._disableInput(\n                            $parent,\n                            excluded_ptav,\n                            current_ptav,\n                            combinationData.mapped_attribute_names\n                        );\n                    });\n                }\n            });\n        }\n        // combination exclusions: array of array of ptav\n        // for example a product with 3 variation and one specific variation is disabled (archived)\n        //  requires the first 2 to be selected for the third to be disabled\n        if (combinationData.archived_combinations) {\n            combinationData.archived_combinations.forEach((excludedCombination) => {\n                const ptavCommon = excludedCombination.filter((ptav) => combination.includes(ptav));\n                if (\n                    !!ptavCommon\n                    && (combination.length === excludedCombination.length)\n                    && (ptavCommon.length === combination.length)\n                ) {\n                    // Selected combination is archived, all attributes must be disabled from each other\n                    combination.forEach((ptav) => {\n                        combination.forEach((ptavOther) => {\n                            if (ptav === ptavOther) {\n                                return;\n                            }\n                            self._disableInput(\n                                $parent,\n                                ptav,\n                                ptavOther,\n                                combinationData.mapped_attribute_names,\n                            );\n                        })\n                    })\n                } else if (\n                    !!ptavCommon\n                    && (combination.length === excludedCombination.length)\n                    && (ptavCommon.length === (combination.length - 1))\n                ) {\n                    // In this case we only need to disable the remaining ptav\n                    const disabledPtav = excludedCombination.find((ptav) => !combination.includes(ptav));\n                    excludedCombination.forEach((ptav) => {\n                        if (ptav === disabledPtav) {\n                            return;\n                        }\n                        self._disableInput(\n                            $parent,\n                            disabledPtav,\n                            ptav,\n                            combinationData.mapped_attribute_names,\n                        )\n                    });\n                }\n            });\n        }\n\n        // parent exclusions (tell which attributes are excluded from parent)\n        for (const [excluded_by, exclusions] of Object.entries(\n            combinationData.parent_exclusions || {}\n        )) {\n            // check that the selected combination is in the parent exclusions\n            exclusions.forEach((ptav) => {\n                // disable the excluded input (even when not already selected)\n                // to give a visual feedback before click\n                self._disableInput(\n                    $parent,\n                    ptav,\n                    excluded_by,\n                    combinationData.mapped_attribute_names,\n                    combinationData.parent_product_name\n                );\n            });\n        }\n    },\n    /**\n     * Extracted to a method to be extendable by other modules\n     *\n     * @param {$.Element} $parent\n     */\n    _getProductId: function ($parent) {\n        return parseInt($parent.find('.product_id').val());\n    },\n    /**\n     * Will disable the input/option that refers to the passed attributeValueId.\n     * This is used for showing the user that some combinations are not available.\n     *\n     * It will also display a message explaining why the input is not selectable.\n     * Based on the \"excludedBy\" and the \"productName\" params.\n     * e.g: Not available with Color: Black\n     *\n     * @private\n     * @param {$.Element} $parent\n     * @param {integer} attributeValueId\n     * @param {integer} excludedBy The attribute value that excludes this input\n     * @param {Object} attributeNames A dict containing all the names of the attribute values\n     *   to show a human readable message explaining why the input is disabled.\n     * @param {string} [productName] The parent product. If provided, it will be appended before\n     *   the name of the attribute value that excludes this input\n     *   e.g: Not available with Customizable Desk (Color: Black)\n     */\n    _disableInput: function ($parent, attributeValueId, excludedBy, attributeNames, productName) {\n        var $input = $parent\n            .find('option[value=' + attributeValueId + '], input[value=' + attributeValueId + ']');\n        $input.addClass('css_not_available');\n        $input.closest('label').addClass('css_not_available');\n        $input.closest('.o_variant_pills').addClass('css_not_available');\n\n        if (excludedBy && attributeNames) {\n            var $target = $input.is('option') ? $input : $input.closest('label').add($input);\n            var excludedByData = [];\n            if ($target.data('excluded-by')) {\n                excludedByData = JSON.parse($target.data('excluded-by'));\n            }\n\n            var excludedByName = attributeNames[excludedBy];\n            if (productName) {\n                excludedByName = productName + ' (' + excludedByName + ')';\n            }\n            excludedByData.push(excludedByName);\n\n            $target.attr('title', _t('Not available with %s', excludedByData.join(', ')));\n            $target.data('excluded-by', JSON.stringify(excludedByData));\n        }\n    },\n    /**\n     * @see onChangeVariant\n     *\n     * @private\n     * @param {MouseEvent} ev\n     * @param {$.Element} $parent\n     * @param {Array} combination\n     */\n    _onChangeCombination: function (ev, $parent, combination) {\n        const $pricePerUom = $parent.find(\".o_base_unit_price:first .oe_currency_value\");\n        if ($pricePerUom) {\n            if (combination.is_combination_possible !== false && combination.base_unit_price != 0) {\n                $pricePerUom.parents(\".o_base_unit_price_wrapper\").removeClass(\"d-none\");\n                $pricePerUom.text(this._priceToStr(combination.base_unit_price));\n                $parent.find(\".oe_custom_base_unit:first\").text(combination.base_unit_name);\n            } else {\n                $pricePerUom.parents(\".o_base_unit_price_wrapper\").addClass(\"d-none\");\n            }\n        }\n\n        // Triggers a new JS event with the correct payload, which is then handled\n        // by the google analytics tracking code.\n        // Indeed, every time another variant is selected, a new view_item event\n        // needs to be tracked by google analytics.\n        if ('product_tracking_info' in combination) {\n            const $product = $('#product_detail');\n            $product.data('product-tracking-info', combination['product_tracking_info']);\n            $product.trigger('view_item_event', combination['product_tracking_info']);\n        }\n        const addToCart = $parent.find('#add_to_cart_wrap');\n        const contactUsButton = $parent.find('#contact_us_wrapper');\n        const productPrice = $parent.find('.product_price');\n        const quantity = $parent.find('.css_quantity');\n        const product_unavailable = $parent.find('#product_unavailable');\n        if (combination.prevent_zero_price_sale) {\n            productPrice.removeClass('d-inline-block').addClass('d-none');\n            quantity.removeClass('d-inline-flex').addClass('d-none');\n            addToCart.removeClass('d-inline-flex').addClass('d-none');\n            contactUsButton.removeClass('d-none').addClass('d-flex');\n            product_unavailable.removeClass('d-none').addClass('d-flex')\n        } else {\n            productPrice.removeClass('d-none').addClass('d-inline-block');\n            quantity.removeClass('d-none').addClass('d-inline-flex');\n            addToCart.removeClass('d-none').addClass('d-inline-flex');\n            contactUsButton.removeClass('d-flex').addClass('d-none');\n            product_unavailable.removeClass('d-flex').addClass('d-none')\n        }\n\n        var self = this;\n        var $price = $parent.find(\".oe_price:first .oe_currency_value\");\n        var $default_price = $parent.find(\".oe_default_price:first .oe_currency_value\");\n        var $compare_price = $parent.find(\".oe_compare_list_price\")\n        $price.text(self._priceToStr(combination.price));\n        $default_price.text(self._priceToStr(combination.list_price));\n\n        var isCombinationPossible = true;\n        if (typeof combination.is_combination_possible !== \"undefined\") {\n            isCombinationPossible = combination.is_combination_possible;\n        }\n        this._toggleDisable($parent, isCombinationPossible);\n\n        if (combination.has_discounted_price && !combination.compare_list_price) {\n            $default_price\n                .closest('.oe_website_sale')\n                .addClass(\"discount\");\n            $default_price.parent().removeClass('d-none');\n            $compare_price.addClass(\"d-none\");\n        } else {\n            $default_price\n                .closest('.oe_website_sale')\n                .removeClass(\"discount\");\n            $default_price.parent().addClass('d-none');\n            $compare_price.removeClass(\"d-none\");\n        }\n\n        var rootComponentSelectors = [\n            'tr.js_product',\n            '.oe_website_sale',\n        ];\n\n        // update images only when changing product\n        // or when either ids are 'false', meaning dynamic products.\n        // Dynamic products don't have images BUT they may have invalid\n        // combinations that need to disable the image.\n        if (!combination.product_id ||\n            !this.last_product_id ||\n            combination.product_id !== this.last_product_id) {\n            this.last_product_id = combination.product_id;\n            self._updateProductImage(\n                $parent.closest(rootComponentSelectors.join(', ')),\n                combination.display_image,\n                combination.product_id,\n                combination.product_template_id,\n                combination.carousel,\n                isCombinationPossible\n            );\n        }\n\n        $parent\n            .find('.product_id')\n            .first()\n            .val(combination.product_id || 0)\n            .trigger('change');\n\n        $parent\n            .find('.o_product_tags')\n            .first()\n            .html(combination.product_tags);\n\n        this.handleCustomValues($(ev.target));\n    },\n\n    /**\n     * returns the formatted price\n     *\n     * @private\n     * @param {float} price\n     */\n    _priceToStr: function (price) {\n        var precision = 2;\n\n        if ($('.decimal_precision').length) {\n            precision = parseInt($('.decimal_precision').last().data('precision'));\n        }\n        var formatted = price.toFixed(precision).split(\".\");\n        const { thousandsSep, decimalPoint, grouping } = localization;\n        formatted[0] = insertThousandsSep(formatted[0], thousandsSep, grouping);\n        return formatted.join(decimalPoint);\n    },\n    /**\n     * Returns a throttled `_getCombinationInfo` with a leading and a trailing\n     * call, which is memoized per `uniqueId`, and for which previous results\n     * are dropped.\n     *\n     * The uniqueId is needed because on the configurator modal there might be\n     * multiple elements triggering the rpc at the same time, and we need each\n     * individual product rpc to be executed, but only once per individual\n     * product.\n     *\n     * The leading execution is to keep good reactivity on the first call, for\n     * a better user experience. The trailing is because ultimately only the\n     * information about the last selected combination is useful. All\n     * intermediary rpc can be ignored and are therefore best not done at all.\n     *\n     * The keepLast is to make sure we only consider the result of the last call, when several\n     * (asynchronous) calls are done in parallel.\n     *\n     * @private\n     * @param {string} uniqueId\n     * @returns {function}\n     */\n    _throttledGetCombinationInfo: memoize(function (self, uniqueId) {\n        const keepLast = new KeepLast();\n        var _getCombinationInfo = throttleForAnimation(self._getCombinationInfo.bind(self));\n        return (ev, params) => keepLast.add(_getCombinationInfo(ev, params));\n    }),\n    /**\n     * Toggles the disabled class depending on the $parent element\n     * and the possibility of the current combination.\n     *\n     * @private\n     * @param {$.Element} $parent\n     * @param {boolean} isCombinationPossible\n     */\n    _toggleDisable: function ($parent, isCombinationPossible) {\n        $parent.toggleClass('css_not_available', !isCombinationPossible);\n    },\n    /**\n     * Updates the product image.\n     * This will use the productId if available or will fallback to the productTemplateId.\n     *\n     * @private\n     * @param {$.Element} $productContainer\n     * @param {boolean} displayImage will hide the image if true. It will use the 'invisible' class\n     *   instead of d-none to prevent layout change\n     * @param {integer} product_id\n     * @param {integer} productTemplateId\n     */\n    _updateProductImage: function ($productContainer, displayImage, productId, productTemplateId) {\n        var model = productId ? 'product.product' : 'product.template';\n        var modelId = productId || productTemplateId;\n        var imageUrl = '/web/image/{0}/{1}/' + (this._productImageField ? this._productImageField : 'image_1024');\n        var imageSrc = imageUrl\n            .replace(\"{0}\", model)\n            .replace(\"{1}\", modelId);\n\n        var imagesSelectors = [\n            'span[data-oe-model^=\"product.\"][data-oe-type=\"image\"] img:first',\n            'img.product_detail_img',\n        ];\n\n        var $img = $productContainer.find(imagesSelectors.join(', '));\n\n        if (displayImage) {\n            $img.removeClass('invisible').attr('src', imageSrc);\n        } else {\n            $img.addClass('invisible');\n        }\n    },\n\n    /**\n     * Highlight selected color\n     *\n     * @private\n     * @param {MouseEvent} ev\n     */\n    _onChangeColorAttribute: function (ev) {\n        var $parent = $(ev.target).closest('.js_product');\n        $parent.find('.css_attribute_color')\n            .removeClass(\"active\")\n            .filter(':has(input:checked)')\n            .addClass(\"active\");\n    },\n\n    _onChangePillsAttribute: function (ev) {\n        const radio = ev.target.closest('.o_variant_pills').querySelector(\"input\");\n        radio.click();  // Trigger onChangeVariant.\n        var $parent = $(ev.target).closest('.js_product');\n        $parent.find('.o_variant_pills')\n            .removeClass(\"active\")\n            .filter(':has(input:checked)')\n            .addClass(\"active\");\n    },\n\n    /**\n     * Return true if the current object has been destroyed. Useful to know if\n     * the result of a rpc should be handled.\n     *\n     * @private\n     */\n    _shouldIgnoreRpcResult() {\n        return (typeof this.isDestroyed === \"function\" && this.isDestroyed());\n    },\n\n    /**\n     * Extension point for website_sale\n     *\n     * @private\n     * @param {string} uri The uri to adapt\n     */\n    _getUri: function (uri) {\n        return uri;\n    }\n};\n\nexport default VariantMixin;\n", "/** @odoo-module **/\n\nimport { Component } from '@odoo/owl';\nimport publicWidget from '@web/legacy/js/public/public_widget';\n\npublicWidget.registry.TermsAndConditionsCheckbox = publicWidget.Widget.extend({\n        selector: 'div[name=\"website_sale_terms_and_conditions_checkbox\"]',\n        events: {\n            'change #website_sale_tc_checkbox': '_onClickTCCheckbox',\n        },\n\n        async start() {\n            this.checkbox = this.el.querySelector('#website_sale_tc_checkbox');\n            return this._super(...arguments);\n        },\n\n        /**\n         * Enable/disabled the payment button when the \"Terms and Conditions\" checkbox is\n         * checked/unchecked.\n         *\n         * @private\n         * @return {void}\n         */\n        _onClickTCCheckbox() {\n            if (this.checkbox.checked) {\n                Component.env.bus.trigger('enablePaymentButton');\n            } else {\n                Component.env.bus.trigger('disablePaymentButton');\n            }\n        },\n\n});\n\nexport default publicWidget.registry.TermsAndConditionsCheckbox;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport VariantMixin from \"@website_sale/js/sale_variant_mixin\";\nimport wSaleUtils from \"@website_sale/js/website_sale_utils\";\nconst cartHandlerMixin = wSaleUtils.cartHandlerMixin;\nimport \"@website/libs/zoomodoo/zoomodoo\";\nimport {extraMenuUpdateCallbacks} from \"@website/js/content/menu\";\nimport { ProductImageViewer } from \"@website_sale/js/components/website_sale_image_viewer\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { debounce, throttleForAnimation } from \"@web/core/utils/timing\";\nimport { listenSizeChange, SIZES, utils as uiUtils } from \"@web/core/ui/ui_service\";\nimport { isBrowserFirefox, hasTouch } from \"@web/core/browser/feature_detection\";\nimport { Component } from \"@odoo/owl\";\n\nexport const WebsiteSale = publicWidget.Widget.extend(VariantMixin, cartHandlerMixin, {\n    selector: '.oe_website_sale',\n    events: Object.assign({}, VariantMixin.events || {}, {\n        'change form .js_product:first input[name=\"add_qty\"]': '_onChangeAddQuantity',\n        'mouseup .js_publish': '_onMouseupPublish',\n        'touchend .js_publish': '_onMouseupPublish',\n        'change .oe_cart input.js_quantity[data-product-id]': '_onChangeCartQuantity',\n        'click .oe_cart a.js_add_suggested_products': '_onClickSuggestedProduct',\n        'click a.js_add_cart_json': '_onClickAddCartJSON',\n        'click .a-submit': '_onClickSubmit',\n        'change form.js_attributes input, form.js_attributes select': '_onChangeAttribute',\n        'mouseup form.js_add_cart_json label': '_onMouseupAddCartLabel',\n        'touchend form.js_add_cart_json label': '_onMouseupAddCartLabel',\n        'submit .o_wsale_products_searchbar_form': '_onSubmitSaleSearch',\n        'click .toggle_summary': '_onToggleSummary',\n        'click #add_to_cart, .o_we_buy_now, #products_grid .o_wsale_product_btn .a-submit': 'async _onClickAdd',\n        'click input.js_product_change': 'onChangeVariant',\n        'change .js_main_product [data-attribute_exclusions]': 'onChangeVariant',\n        'click .o_product_page_reviews_link': '_onClickReviewsLink',\n        'mousedown .o_wsale_filmstip_wrapper': '_onMouseDown',\n        'mouseleave .o_wsale_filmstip_wrapper': '_onMouseLeave',\n        'mouseup .o_wsale_filmstip_wrapper': '_onMouseUp',\n        'mousemove .o_wsale_filmstip_wrapper': '_onMouseMove',\n        'click .o_wsale_filmstip_wrapper' : '_onClickHandler',\n        'submit': '_onClickConfirmOrder',\n    }),\n\n    /**\n     * @constructor\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n\n        this._changeCartQuantity = debounce(this._changeCartQuantity.bind(this), 500);\n\n        this.isWebsite = true;\n        this.filmStripStartX = 0;\n        this.filmStripIsDown = false;\n        this.filmStripScrollLeft = 0;\n        this.filmStripMoved = false;\n\n        delete this.events['change [data-attribute_exclusions]'];\n    },\n    /**\n     * @override\n     */\n    start() {\n        const def = this._super(...arguments);\n\n        this._applyHash();\n\n        // This has to be triggered to compute the \"out of stock\" feature and the hash variant changes\n        this.triggerVariantChange(this.$el);\n\n        listenSizeChange(() => {\n            if (uiUtils.getSize() === SIZES.XL) {\n                $('.toggle_summary_div').addClass('d-none d-xl-block');\n            }\n        })\n\n        this._startZoom();\n\n        // Triggered when selecting a variant of a product in a carousel element\n        window.addEventListener(\"hashchange\", (ev) => {\n            this._applyHash();\n            this.triggerVariantChange(this.$el);\n        });\n\n        // This allows conditional styling for the filmstrip\n        const filmstripContainer = this.el.querySelector('.o_wsale_filmstip_container');\n        const filmstripContainerWidth = filmstripContainer\n            ? filmstripContainer.getBoundingClientRect().width : 0;\n        const filmstripWrapper = this.el.querySelector('.o_wsale_filmstip_wrapper');\n        const filmstripWrapperWidth = filmstripWrapper\n            ? filmstripWrapper.getBoundingClientRect().width : 0;\n        const isFilmstripScrollable = filmstripWrapperWidth < filmstripContainerWidth\n        if (isBrowserFirefox() || hasTouch() || isFilmstripScrollable) {\n            filmstripContainer?.classList.add('o_wsale_filmstip_fancy_disabled');\n        }\n\n        this.getRedirectOption();\n        return def;\n    },\n    destroy() {\n        this._super.apply(this, arguments);\n        this._cleanupZoom();\n    },\n    /**\n     * The selector is different when using list view of variants.\n     *\n     * @override\n     */\n    getSelectedVariantValues: function ($container) {\n        var combination = $container.find('input.js_product_change:checked')\n            .data('combination');\n\n        if (combination) {\n            return combination;\n        }\n        return VariantMixin.getSelectedVariantValues.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _onMouseDown: function (ev) {\n        this.filmStripIsDown = true;\n        this.filmStripStartX = ev.pageX - ev.currentTarget.offsetLeft;\n        this.filmStripScrollLeft = ev.currentTarget.scrollLeft;\n        this.formerTarget = ev.target;\n        this.filmStripMoved = false;\n    },\n    _onMouseLeave: function (ev) {\n        if (!this.filmStripIsDown) {\n            return;\n        }\n        ev.currentTarget.classList.remove('activeDrag');\n        this.filmStripIsDown = false\n    },\n    _onMouseUp: function (ev) {\n        this.filmStripIsDown = false;\n        ev.currentTarget.classList.remove('activeDrag');\n    },\n    _onMouseMove: function (ev) {\n        if (!this.filmStripIsDown) {\n            return;\n        }\n        ev.preventDefault();\n        ev.currentTarget.classList.add('activeDrag');\n        this.filmStripMoved = true;\n        const x = ev.pageX - ev.currentTarget.offsetLeft;\n        const walk = (x - this.filmStripStartX) * 2;\n        ev.currentTarget.scrollLeft = this.filmStripScrollLeft - walk;\n    },\n    _onClickHandler: function(ev) {\n        if(this.filmStripMoved) {\n            ev.stopPropagation();\n            ev.preventDefault();\n        }\n    },\n    _applyHash: function () {\n        const params = new URLSearchParams(window.location.hash.substring(1));\n        if (params.get(\"attribute_values\")) {\n            const attributeValueIds = params.get(\"attribute_values\").split(',');\n            const inputs = document.querySelectorAll(\n                'input.js_variant_change, select.js_variant_change option'\n            );\n            inputs.forEach((element) => {\n                if (attributeValueIds.includes(element.dataset.attributeValueId)) {\n                    if (element.tagName === \"INPUT\") {\n                        element.checked = true;\n                    } else if (element.tagName === \"OPTION\") {\n                        element.selected = true;\n                    }\n                }\n            });\n            this._changeAttribute(['.css_attribute_color', '.o_variant_pills']);\n        }\n    },\n\n    /**\n     * Sets the url hash from the selected product options.\n     *\n     * @private\n     */\n    _setUrlHash: function ($parent) {\n        const inputs = document.querySelectorAll(\n            'input.js_variant_change:checked, select.js_variant_change option:checked'\n        );\n        let attributeIds = [];\n        inputs.forEach((element) => attributeIds.push(element.dataset.attributeValueId));\n        if (attributeIds.length > 0) {\n            window.location.hash = `attribute_values=${attributeIds.join(',')}`;\n        }\n    },\n    /**\n     * Set the checked values active.\n     *\n     * @private\n     * @param {Array} valueSelectors Selectors\n     */\n    _changeAttribute: function (valueSelectors) {\n        valueSelectors.forEach((selector) => {\n            $(selector).removeClass(\"active\").filter(\":has(input:checked)\").addClass(\"active\");\n        });\n    },\n    /**\n     * @private\n     */\n    _changeCartQuantity: function ($input, value, $dom_optional, line_id, productIDs) {\n        $($dom_optional).toArray().forEach((elem) => {\n            $(elem).find('.js_quantity').text(value);\n            productIDs.push($(elem).find('span[data-product-id]').data('product-id'));\n        });\n        $input.data('update_change', true);\n\n        rpc(\"/shop/cart/update_json\", {\n            line_id: line_id,\n            product_id: parseInt($input.data('product-id'), 10),\n            set_qty: value,\n            display: true,\n        }).then((data) => {\n            $input.data('update_change', false);\n            var check_value = parseInt($input.val() || 0, 10);\n            if (isNaN(check_value)) {\n                check_value = 1;\n            }\n            if (value !== check_value) {\n                $input.trigger('change');\n                return;\n            }\n            if (!data.cart_quantity) {\n                return window.location = '/shop/cart';\n            }\n            $input.val(data.quantity);\n            $('.js_quantity[data-line-id='+line_id+']').val(data.quantity).text(data.quantity);\n\n            wSaleUtils.updateCartNavBar(data);\n            wSaleUtils.showWarning(data.notification_info.warning);\n            // Propagating the change to the express checkout forms\n            Component.env.bus.trigger('cart_amount_changed', [data.amount, data.minor_amount]);\n        });\n    },\n    /**\n     * This is overridden to handle the \"List View of Variants\" of the web shop.\n     * That feature allows directly selecting the variant from a list instead of selecting the\n     * attribute values.\n     *\n     * Since the layout is completely different, we need to fetch the product_id directly\n     * from the selected variant.\n     *\n     * @override\n     */\n    _getProductId: function ($parent) {\n        if ($parent.find('input.js_product_change').length !== 0) {\n            return parseInt($parent.find('input.js_product_change:checked').val());\n        }\n        else {\n            return VariantMixin._getProductId.apply(this, arguments);\n        }\n    },\n    _getProductImageLayout: function () {\n        return document.querySelector(\"#product_detail_main\").dataset.image_layout;\n    },\n    _getProductImageWidth: function () {\n        return document.querySelector(\"#product_detail_main\").dataset.image_width;\n    },\n    _getProductImageContainerSelector: function () {\n        return {\n            'carousel': \"#o-carousel-product\",\n            'grid': \"#o-grid-product\",\n        }[this._getProductImageLayout()];\n    },\n    _getProductImageContainer: function () {\n        return document.querySelector(this._getProductImageContainerSelector());\n    },\n    _isEditorEnabled() {\n        return document.body.classList.contains(\"editor_enable\");\n    },\n    /**\n     * @private\n     */\n    _startZoom: function () {\n        const salePage = document.querySelector(\".o_wsale_product_page\");\n        if (!salePage || this._getProductImageWidth() === \"none\") {\n            return;\n        }\n        this._cleanupZoom();\n        this.zoomCleanup = [];\n        // Zoom on hover (except on mobile)\n        if (salePage.dataset.ecomZoomAuto && !uiUtils.isSmall()) {\n            const images = salePage.querySelectorAll(\"img[data-zoom]\");\n            for (const image of images) {\n                const $image = $(image);\n                const callback = () => {\n                    $image.zoomOdoo({\n                        event: \"mouseenter\",\n                        attach: this._getProductImageContainerSelector(),\n                        preventClicks: salePage.dataset.ecomZoomClick,\n                        attachToTarget: this._getProductImageLayout() === \"grid\",\n                    });\n                    image.dataset.zoom = 1;\n                };\n                image.addEventListener('load', callback);\n                this.zoomCleanup.push(() => {\n                    image.removeEventListener('load', callback);\n                    const zoomOdoo = $image.data(\"zoomOdoo\");\n                    if (zoomOdoo) {\n                        zoomOdoo.hide();\n                        $image.unbind();\n                    }\n                });\n                if (image.complete) {\n                    callback();\n                }\n            }\n        }\n        // Zoom on click\n        if (salePage.dataset.ecomZoomClick) {\n            // In this case we want all the images not just the ones that are \"zoomables\"\n            const images = salePage.querySelectorAll(\".product_detail_img\");\n            for (const image of images ) {\n                const handler = () => {\n                    if (salePage.dataset.ecomZoomAuto) {\n                        // Remove any flyout\n                        const flyouts = document.querySelectorAll(\".zoomodoo-flyout\");\n                        for (const flyout of flyouts) {\n                            flyout.remove();\n                        }\n                    }\n                    this.call(\"dialog\", \"add\", ProductImageViewer, {\n                        selectedImageIdx: [...images].indexOf(image),\n                        images,\n                    });\n                };\n                image.addEventListener(\"click\", handler);\n                this.zoomCleanup.push(() => {\n                    image.removeEventListener(\"click\", handler);\n                });\n            }\n        }\n    },\n    _cleanupZoom() {\n        if (!this.zoomCleanup || !this.zoomCleanup.length) {\n            return;\n        }\n        for (const cleanup of this.zoomCleanup) {\n            cleanup();\n        }\n        this.zoomCleanup = undefined;\n    },\n    /**\n     * On website, we display a carousel instead of only one image\n     *\n     * @override\n     * @private\n     */\n    _updateProductImage: function ($productContainer, displayImage, productId, productTemplateId, newImages, isCombinationPossible) {\n        let $images = $productContainer.find(this._getProductImageContainerSelector());\n        // When using the web editor, don't reload this or the images won't\n        // be able to be edited depending on if this is done loading before\n        // or after the editor is ready.\n        if ($images.length && !this._isEditorEnabled()) {\n            const $newImages = $(newImages);\n            $images.after($newImages);\n            $images.remove();\n            $images = $newImages;\n            // Update the sharable image (only work for Pinterest).\n            const shareImageSrc = $images[0].querySelector('img').src;\n            document.querySelector('meta[property=\"og:image\"]')\n                .setAttribute('content', shareImageSrc);\n\n            if ($images.attr('id') === 'o-carousel-product') {\n                $images.carousel(0);\n            }\n            this._startZoom();\n            // fix issue with carousel height\n            this.trigger_up('widgets_start_request', {$target: $images});\n        }\n        $images.toggleClass('css_not_available', !isCombinationPossible);\n    },\n    /**\n     * @private\n     * @param {MouseEvent} ev\n     */\n    _onClickAdd: function (ev) {\n        ev.preventDefault();\n        var def = () => {\n            this.getCartHandlerOptions(ev);\n            return this._handleAdd($(ev.currentTarget).closest('form'));\n        };\n        if ($('.js_add_cart_variants').children().length) {\n            return this._getCombinationInfo(ev).then(() => {\n                return !$(ev.target).closest('.js_product').hasClass(\"css_not_available\") ? def() : Promise.resolve();\n            });\n        }\n        return def();\n    },\n    /**\n     * Initializes the optional products modal\n     * and add handlers to the modal events (confirm, back, ...)\n     *\n     * @private\n     * @param {$.Element} $form the related webshop form\n     */\n    _handleAdd: function ($form) {\n        var self = this;\n        this.$form = $form;\n\n        var productSelector = [\n            'input[type=\"hidden\"][name=\"product_id\"]',\n            'input[type=\"radio\"][name=\"product_id\"]:checked'\n        ];\n\n        const productTemplateId =\n            parseInt($form.find('input[type=\"hidden\"][name=\"product_template_id\"]').first().val());\n        var productReady = this.selectOrCreateProduct(\n            $form,\n            parseInt($form.find(productSelector.join(', ')).first().val(), 10),\n            productTemplateId,\n        );\n\n        return productReady.then(function (productId) {\n            $form.find(productSelector.join(', ')).val(productId);\n            self._updateRootProduct($form, productId, productTemplateId);\n            return self._onProductReady($form.closest('.o_wsale_product_page').length > 0);\n        });\n    },\n\n    _onProductReady(isOnProductPage = false) {\n        return this._submitForm();\n    },\n\n    /**\n     * Add custom variant values and attribute values that do not generate variants\n     * in the params to submit form if 'stay on page' option is disabled, or call\n     * '_addToCartInPage' otherwise.\n     *\n     * @private\n     * @returns {Promise}\n     */\n    _submitForm: function () {\n        const params = this.rootProduct;\n\n        const $product = $('#product_detail');\n        const productTrackingInfo = $product.data('product-tracking-info');\n        if (productTrackingInfo) {\n            productTrackingInfo.quantity = params.quantity;\n            $product.trigger('add_to_cart_event', [productTrackingInfo]);\n        }\n\n        params.add_qty = params.quantity;\n        params.product_custom_attribute_values = JSON.stringify(params.product_custom_attribute_values);\n        params.no_variant_attribute_values = JSON.stringify(params.no_variant_attribute_values);\n        delete params.quantity;\n        return this.addToCart(params);\n    },\n    /**\n     * @private\n     * @param {MouseEvent} ev\n     */\n    _onClickAddCartJSON: function (ev) {\n        this.onClickAddCartJSON(ev);\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onChangeAddQuantity: function (ev) {\n        this.onChangeAddQuantity(ev);\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onMouseupPublish: function (ev) {\n        $(ev.currentTarget).parents('.thumbnail').toggleClass('disabled');\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onChangeCartQuantity: function (ev) {\n        var $input = $(ev.currentTarget);\n        if ($input.data('update_change')) {\n            return;\n        }\n        var value = parseInt($input.val() || 0, 10);\n        if (isNaN(value)) {\n            value = 1;\n        }\n        var $dom = $input.closest('tr');\n        // var default_price = parseFloat($dom.find('.text-danger > span.oe_currency_value').text());\n        var $dom_optional = $dom.nextUntil(':not(.optional_product.info)');\n        var line_id = parseInt($input.data('line-id'), 10);\n        var productIDs = [parseInt($input.data('product-id'), 10)];\n        this._changeCartQuantity($input, value, $dom_optional, line_id, productIDs);\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onClickSuggestedProduct: function (ev) {\n        $(ev.currentTarget).prev('input').val(1).trigger('change');\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onClickSubmit: function (ev) {\n        if ($(ev.currentTarget).is('#add_to_cart, #products_grid .a-submit')) {\n            return;\n        }\n        var $aSubmit = $(ev.currentTarget);\n        if (!ev.defaultPrevented && !$aSubmit.is(\".disabled\")) {\n            ev.preventDefault();\n            $aSubmit.closest('form').submit();\n        }\n        if ($aSubmit.hasClass('a-submit-disable')) {\n            $aSubmit.addClass(\"disabled\");\n        }\n        if ($aSubmit.hasClass('a-submit-loading')) {\n            var loading = '<span class=\"fa fa-cog fa-spin\"/>';\n            var fa_span = $aSubmit.find('span[class*=\"fa\"]');\n            if (fa_span.length) {\n                fa_span.replaceWith(loading);\n            } else {\n                $aSubmit.append(loading);\n            }\n        }\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onChangeAttribute: function (ev) {\n        if (!ev.defaultPrevented) {\n            ev.preventDefault();\n            const productGrid = this.el.querySelector(\".o_wsale_products_grid_table_wrapper\");\n            if (productGrid) {\n                productGrid.classList.add(\"opacity-50\");\n            }\n            $(ev.currentTarget).closest(\"form\").submit();\n        }\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onMouseupAddCartLabel: function (ev) { // change price when they are variants\n        var $label = $(ev.currentTarget);\n        var $price = $label.parents(\"form:first\").find(\".oe_price .oe_currency_value\");\n        if (!$price.data(\"price\")) {\n            $price.data(\"price\", parseFloat($price.text()));\n        }\n        var value = $price.data(\"price\") + parseFloat($label.find(\".badge span\").text() || 0);\n\n        var dec = value % 1;\n        $price.html(value + (dec < 0.01 ? \".00\" : (dec < 1 ? \"0\" : \"\") ));\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onSubmitSaleSearch: function (ev) {\n        if (!this.$('.dropdown_sorty_by').length) {\n            return;\n        }\n        var $this = $(ev.currentTarget);\n        if (!ev.defaultPrevented && !$this.is(\".disabled\")) {\n            ev.preventDefault();\n            var oldurl = $this.attr('action');\n            oldurl += (oldurl.indexOf(\"?\")===-1) ? \"?\" : \"\";\n            if ($this.find('[name=noFuzzy]').val() === \"true\") {\n                oldurl += '&noFuzzy=true';\n            }\n            var search = $this.find('input.search-query');\n            window.location = oldurl + '&' + search.attr('name') + '=' + encodeURIComponent(search.val());\n        }\n    },\n    /**\n     * Toggles the add to cart button depending on the possibility of the\n     * current combination.\n     *\n     * @override\n     */\n    _toggleDisable: function ($parent, isCombinationPossible) {\n        VariantMixin._toggleDisable.apply(this, arguments);\n        $parent.find(\"#add_to_cart\").toggleClass('disabled', !isCombinationPossible);\n        $parent.find(\".o_we_buy_now\").toggleClass('disabled', !isCombinationPossible);\n    },\n    /**\n     * Write the properties of the form elements in the DOM to prevent the\n     * current selection from being lost when activating the web editor.\n     *\n     * @override\n     */\n    onChangeVariant: function (ev) {\n        var $component = $(ev.currentTarget).closest('.js_product');\n        $component.find('input').each(function () {\n            var $el = $(this);\n            $el.attr('checked', $el.is(':checked'));\n        });\n        $component.find('select option').each(function () {\n            var $el = $(this);\n            $el.attr('selected', $el.is(':selected'));\n        });\n\n        this._setUrlHash($component);\n\n        return VariantMixin.onChangeVariant.apply(this, arguments);\n    },\n    /**\n     * @private\n     */\n    _onToggleSummary: function () {\n        $('.toggle_summary_div').toggleClass('d-none');\n        $('.toggle_summary_div').removeClass('d-xl-block');\n    },\n    /**\n     * @private\n     */\n    _onClickReviewsLink: function () {\n        $('#o_product_page_reviews_content').collapse('show');\n    },\n    /**\n     * Prevent multiclicks on confirm button when the form is submitted\n     *\n     * @private\n     */\n    _onClickConfirmOrder: function () {\n        // FIXME ANVFE this should only be triggered when we effectively click on that specific button no?\n        // should not impact the address page at least\n        const submitFormButton = $('form[name=\"o_wsale_confirm_order\"]').find('button[type=\"submit\"]');\n        submitFormButton.attr('disabled', true);\n        setTimeout(() => submitFormButton.attr('disabled', false), 5000);\n    },\n\n    // -------------------------------------\n    // Utils\n    // -------------------------------------\n    /**\n     * Update the root product during an Add process.\n     *\n     * @private\n     * @param {Object} $form\n     * @param {Number} productId\n     * @param {Number} productTemplateId\n     */\n    _updateRootProduct($form, productId, productTemplateId) {\n        this.rootProduct = {\n            product_id: productId,\n            product_template_id: productTemplateId,\n            quantity: parseFloat($form.find('input[name=\"add_qty\"]').val() || 1),\n            product_custom_attribute_values: this.getCustomVariantValues($form.find('.js_product')),\n            variant_values: this.getSelectedVariantValues($form.find('.js_product')),\n            no_variant_attribute_values: this.getNoVariantAttributeValues($form.find('.js_product'))\n        };\n    },\n});\n\npublicWidget.registry.WebsiteSale = WebsiteSale\n\npublicWidget.registry.WebsiteSaleLayout = publicWidget.Widget.extend({\n    selector: '.oe_website_sale',\n    disabledInEditableMode: false,\n    events: {\n        'change .o_wsale_apply_layout input': '_onApplyShopLayoutChange',\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onApplyShopLayoutChange: function (ev) {\n        const wysiwyg = this.options.wysiwyg;\n        if (wysiwyg) {\n            wysiwyg.odooEditor.observerUnactive('_onApplyShopLayoutChange');\n        }\n        var clickedValue = $(ev.target).val();\n        var isList = clickedValue === 'list';\n        if (!this.editableMode) {\n            rpc('/shop/save_shop_layout_mode', {\n                'layout_mode': isList ? 'list' : 'grid',\n            });\n        }\n\n        const activeClasses = ev.target.parentElement.dataset.activeClasses.split(' ');\n        ev.target.parentElement.querySelectorAll('.btn').forEach((btn) => {\n            activeClasses.map(c => btn.classList.toggle(c));\n        });\n\n        var $grid = this.$('#products_grid');\n        // Disable transition on all list elements, then switch to the new\n        // layout then reenable all transitions after having forced a redraw\n        // TODO should probably be improved to allow disabling transitions\n        // altogether with a class/option.\n        $grid.find('*').css('transition', 'none');\n        $grid.toggleClass('o_wsale_layout_list', isList);\n        void $grid[0].offsetWidth;\n        $grid.find('*').css('transition', '');\n        if (wysiwyg) {\n            wysiwyg.odooEditor.observerActive('_onApplyShopLayoutChange');\n        }\n    },\n});\n\npublicWidget.registry.WebsiteSaleAccordionProduct = publicWidget.Widget.extend({\n    selector: \"#product_accordion\",\n\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n        this._updateAccordionActiveItem();\n    },\n\n    /**\n     * Replace the .SCSS styling applied awaiting Js for the default bootstrap classes,\n     * opening the first accordion entry and restoring flush behavior.\n     *\n     * @private\n     */\n    _updateAccordionActiveItem() {\n        const firstAccordionItemEl = this.el.querySelector('.accordion-item');\n        if (!firstAccordionItemEl) return;\n\n        const firstAccordionItemButtonEl = firstAccordionItemEl.querySelector('.accordion-button');\n        firstAccordionItemButtonEl.classList.remove('collapsed');\n        firstAccordionItemButtonEl.setAttribute('aria-expanded', 'true');\n        firstAccordionItemEl.querySelector('.accordion-collapse').classList.add('show');\n        this.target.classList.remove('o_accordion_not_initialized');\n    },\n});\n\npublicWidget.registry.websiteSaleCarouselProduct = publicWidget.Widget.extend({\n    selector: '#o-carousel-product',\n    disabledInEditableMode: false,\n    events: {\n        'wheel .o_carousel_product_indicators': '_onMouseWheel',\n    },\n\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n        this._updateCarouselPosition();\n        this.throttleOnResize = throttleForAnimation(this._onSlideCarouselProduct.bind(this));\n        extraMenuUpdateCallbacks.push(this._updateCarouselPosition.bind(this));\n        if (this.$el.find('.carousel-indicators').length > 0) {\n            this.$el.on('slide.bs.carousel.carousel_product_slider', this._onSlideCarouselProduct.bind(this));\n            $(window).on('resize.carousel_product_slider', this.throttleOnResize);\n            this._updateJustifyContent();\n        }\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this.$el.css('top', '');\n        this.$el.off('.carousel_product_slider');\n        if (this.throttleOnResize) {\n            this.throttleOnResize.cancel();\n        }\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _updateCarouselPosition() {\n        let size = 5;\n        for (const el of document.querySelectorAll('.o_top_fixed_element')) {\n            size += $(el).outerHeight();\n        }\n        this.$el.css('top', size);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Center the selected indicator to scroll the indicators list when it\n     * overflows.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onSlideCarouselProduct: function (ev) {\n        const isReversed = this.$el.css('flex-direction') === \"column-reverse\";\n        const isLeftIndicators = this.$el.hasClass('o_carousel_product_left_indicators');\n        const $indicatorsDiv = isLeftIndicators ? this.$el.find('.o_carousel_product_indicators') : this.$el.find('.carousel-indicators');\n        let indicatorIndex = $(ev.relatedTarget).index();\n        indicatorIndex = indicatorIndex > -1 ? indicatorIndex : this.$el.find('li.active').index();\n        const $indicator = $indicatorsDiv.find('[data-bs-slide-to=' + indicatorIndex + ']');\n        const indicatorsDivSize = isLeftIndicators && !isReversed ? $indicatorsDiv.outerHeight() : $indicatorsDiv.outerWidth();\n        const indicatorSize = isLeftIndicators && !isReversed ? $indicator.outerHeight() : $indicator.outerWidth();\n        const indicatorPosition = isLeftIndicators && !isReversed ? $indicator.position().top : $indicator.position().left;\n        const scrollSize = isLeftIndicators && !isReversed ? $indicatorsDiv[0].scrollHeight : $indicatorsDiv[0].scrollWidth;\n        let indicatorsPositionDiff = (indicatorPosition + (indicatorSize/2)) - (indicatorsDivSize/2);\n        indicatorsPositionDiff = Math.min(indicatorsPositionDiff, scrollSize - indicatorsDivSize);\n        this._updateJustifyContent();\n        const indicatorsPositionX = isLeftIndicators && !isReversed ? '0' : '-' + indicatorsPositionDiff;\n        const indicatorsPositionY = isLeftIndicators && !isReversed ? '-' + indicatorsPositionDiff : '0';\n        const translate3D = indicatorsPositionDiff > 0 ? \"translate3d(\" + indicatorsPositionX + \"px,\" + indicatorsPositionY + \"px,0)\" : '';\n        $indicatorsDiv.css(\"transform\", translate3D);\n    },\n    /**\n     * @private\n     */\n     _updateJustifyContent: function () {\n        const $indicatorsDiv = this.$el.find('.carousel-indicators');\n        $indicatorsDiv.css('justify-content', 'start');\n        if (uiUtils.getSize() <= SIZES.MD) {\n            if (($indicatorsDiv.children().last().position().left + this.$el.find('li').outerWidth()) < $indicatorsDiv.outerWidth()) {\n                $indicatorsDiv.css('justify-content', 'center');\n            }\n        }\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onMouseWheel: function (ev) {\n        ev.preventDefault();\n        if (ev.originalEvent.deltaY > 0) {\n            this.$el.carousel('next');\n        } else {\n            this.$el.carousel('prev');\n        }\n    },\n});\n\npublicWidget.registry.websiteSaleProductPageReviews = publicWidget.Widget.extend({\n    selector: '#o_product_page_reviews',\n    disabledInEditableMode: false,\n\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n        this._updateChatterComposerPosition();\n        extraMenuUpdateCallbacks.push(this._updateChatterComposerPosition.bind(this));\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this.$el.find('.o_portal_chatter_composer').css('top', '');\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _updateChatterComposerPosition() {\n        let size = 20;\n        for (const el of document.querySelectorAll('.o_top_fixed_element')) {\n            size += $(el).outerHeight();\n        }\n        this.$el.find('.o_portal_chatter_composer').css('top', size);\n    },\n});\n\nexport default {\n    WebsiteSale: publicWidget.registry.WebsiteSale,\n    WebsiteSaleLayout: publicWidget.registry.WebsiteSaleLayout,\n    WebsiteSaleProductPage: publicWidget.registry.WebsiteSaleAccordionProduct,\n    WebsiteSaleCarouselProduct: publicWidget.registry.websiteSaleCarouselProduct,\n    WebsiteSaleProductPageReviews: publicWidget.registry.websiteSaleProductPageReviews,\n};\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\npublicWidget.registry.websiteSaleOffcanvas = publicWidget.Widget.extend({\n    selector: '#o_wsale_offcanvas',\n    events: {\n        'show.bs.offcanvas': '_toggleFilters',\n        'hidden.bs.offcanvas': '_toggleFilters',\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Unfold active filters, fold inactive ones\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _toggleFilters: function (ev) {\n        for (const btn of this.el.querySelectorAll('button[data-status]')) {\n            if(btn.classList.contains('collapsed') && btn.dataset.status == \"active\" || ! btn.classList.contains('collapsed') && btn.dataset.status == \"inactive\" ) {\n                btn.click();\n            }\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\npublicWidget.registry.multirangePriceSelector = publicWidget.Widget.extend({\n    selector: '.o_wsale_products_page',\n    events: {\n        'newRangeValue #o_wsale_price_range_option input[type=\"range\"]': '_onPriceRangeSelected',\n    },\n\n    //----------------------------------------------------------------------\n    // Handlers\n    //----------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onPriceRangeSelected(ev) {\n        const range = ev.currentTarget;\n        const searchParams = new URLSearchParams(window.location.search);\n        searchParams.delete(\"min_price\");\n        searchParams.delete(\"max_price\");\n        if (parseFloat(range.min) !== range.valueLow) {\n            searchParams.set(\"min_price\", range.valueLow);\n        }\n        if (parseFloat(range.max) !== range.valueHigh) {\n            searchParams.set(\"max_price\", range.valueHigh);\n        }\n        let product_list_div = this.el.querySelector('.o_wsale_products_grid_table_wrapper');\n        if (product_list_div) {\n            product_list_div.classList.add('opacity-50');\n        }\n        window.location.search = searchParams.toString();\n    },\n});\n", "/** @odoo-module **/\n\nimport { rpc } from '@web/core/network/rpc';\nimport { serializeDateTime } from '@web/core/l10n/dates';\nimport {\n    ComboConfiguratorDialog\n} from '@sale/js/combo_configurator_dialog/combo_configurator_dialog';\nimport { ProductCombo } from \"@sale/js/models/product_combo\";\nimport {\n    ProductConfiguratorDialog\n} from '@sale/js/product_configurator_dialog/product_configurator_dialog';\nimport { serializeComboItem } from '@sale/js/sale_utils';\nimport { WebsiteSale } from '@website_sale/js/website_sale';\nimport wSaleUtils from '@website_sale/js/website_sale_utils';\n\nconst { DateTime } = luxon;\n\nWebsiteSale.include({\n\n    _onProductReady(isOnProductPage = false) {\n        return this._openDialog(isOnProductPage);\n    },\n\n    async _openDialog(isOnProductPage) {\n        const { combos, ...remainingData } = await rpc(\n            '/website_sale/combo_configurator/get_data',\n            {\n                product_tmpl_id: this.rootProduct.product_template_id,\n                quantity: this.rootProduct.quantity,\n                date: serializeDateTime(DateTime.now()),\n                ...this._getAdditionalRpcParams(),\n            }\n        );\n        if (combos.length) {\n            return this._openComboConfigurator(combos, remainingData);\n        }\n        if (this.isBuyNow) {\n            return this._submitForm();\n        }\n        const shouldShowProductConfigurator = await rpc(\n            '/website_sale/should_show_product_configurator',\n            {\n                product_template_id: this.rootProduct.product_template_id,\n                ptav_ids: this.rootProduct.variant_values,\n                is_product_configured: isOnProductPage,\n            }\n        );\n        if (shouldShowProductConfigurator) {\n            return this._openProductConfigurator(isOnProductPage);\n        }\n        return this._submitForm();\n    },\n\n    /**\n     * Opens the product configurator dialog.\n     *\n     * @param isOnProductPage Whether the user is currently on the product page.\n     */\n    _openProductConfigurator(isOnProductPage) {\n        this.call('dialog', 'add', ProductConfiguratorDialog, {\n            productTemplateId: this.rootProduct.product_template_id,\n            ptavIds: this.rootProduct.variant_values,\n            customPtavs: this.rootProduct.product_custom_attribute_values.map(\n                customPtav => ({\n                    id: customPtav.custom_product_template_attribute_value_id,\n                    value: customPtav.custom_value,\n                })\n            ),\n            quantity: this.rootProduct.quantity,\n            soDate: serializeDateTime(DateTime.now()),\n            edit: false,\n            isFrontend: true,\n            options: { isMainProductConfigurable: !isOnProductPage },\n            save: async (mainProduct, optionalProducts, options) => {\n                this._trackProducts([mainProduct, ...optionalProducts]);\n\n                const values = await rpc('/website_sale/product_configurator/update_cart', {\n                    main_product: this._serializeProduct(mainProduct),\n                    optional_products: optionalProducts.map(this._serializeProduct),\n                    ...this._getAdditionalRpcParams(),\n                });\n                this._onConfigured(options, values);\n            },\n            discard: () => {},\n            ...this._getAdditionalDialogProps(),\n        });\n    },\n\n    /**\n     * Opens the combo configurator dialog.\n     *\n     * @param combos The combos of the product.\n     * @param remainingData Other data needed to open the combo configurator.\n     */\n    _openComboConfigurator(combos, remainingData) {\n        this.call('dialog', 'add', ComboConfiguratorDialog, {\n            combos: combos.map(combo => new ProductCombo(combo)),\n            ...remainingData,\n            date: serializeDateTime(DateTime.now()),\n            edit: false,\n            isFrontend: true,\n            save: async (comboProductData, selectedComboItems, options) => {\n                this._trackProducts([{\n                    'id': this.rootProduct.product_id,\n                    'display_name': remainingData.display_name,\n                    'category_name': remainingData.category_name,\n                    'currency_name': remainingData.currency_name,\n                    'price': comboProductData.price,\n                    'quantity': comboProductData.quantity,\n                }]);\n\n                const values = await rpc('/website_sale/combo_configurator/update_cart', {\n                    combo_product_id: this.rootProduct.product_id,\n                    quantity: comboProductData.quantity,\n                    selected_combo_items: selectedComboItems.map(serializeComboItem),\n                    ...this._getAdditionalRpcParams(),\n                });\n                this._onConfigured(options, values);\n            },\n            discard: () => {},\n            ...this._getAdditionalDialogProps(),\n        });\n    },\n\n    _onConfigured(options, values) {\n        if (options.goToCart) {\n            window.location.pathname = '/shop/cart';\n        } else {\n            wSaleUtils.updateCartNavBar(values);\n            wSaleUtils.showCartNotification(this.call.bind(this), values.notification_info);\n        }\n        // Reload the product page after adding items to the cart. This is needed, for\n        // example, to update the available stock.\n        this._getCombinationInfo($.Event('click', { target: $('#add_to_cart') }));\n    },\n\n    /**\n     * Hook to append additional props in overriding modules.\n     *\n     * @return {Object} The additional props.\n     */\n    _getAdditionalDialogProps() {\n        return {};\n    },\n\n    /**\n     * Hook to append additional RPC params in overriding modules.\n     *\n     * @return {Object} The additional RPC params.\n     */\n    _getAdditionalRpcParams() {\n        return {};\n    },\n\n    /**\n     * Serialize a product into a format understandable by the server.\n     *\n     * @param {Object} product The product to serialize.\n     * @return {Object} The serialized product.\n     */\n    _serializeProduct(product) {\n        let serializedProduct = {\n            product_id: product.id,\n            product_template_id: product.product_tmpl_id,\n            parent_product_template_id: product.parent_product_tmpl_id,\n            quantity: product.quantity,\n        }\n\n        if (!product.attribute_lines) {\n            return serializedProduct;\n        }\n\n        // Custom attributes.\n        serializedProduct.product_custom_attribute_values = [];\n        for (const ptal of product.attribute_lines) {\n            const selectedPtavIds = new Set(ptal.selected_attribute_value_ids);\n            const selectedCustomPtav = ptal.attribute_values.find(\n                ptav => ptav.is_custom && selectedPtavIds.has(ptav.id)\n            );\n            if (selectedCustomPtav) {\n                serializedProduct.product_custom_attribute_values.push({\n                    custom_product_template_attribute_value_id: selectedCustomPtav.id,\n                    custom_value: ptal.customValue,\n                });\n            }\n        }\n\n        // No variant attributes.\n        serializedProduct.no_variant_attribute_value_ids = product.attribute_lines\n            .filter(ptal => ptal.create_variant === 'no_variant')\n            .flatMap(ptal => ptal.selected_attribute_value_ids);\n\n        return serializedProduct;\n    },\n\n    _trackProducts: function (products) {\n        const productsTrackingInfo = []\n        for (const product of products) {\n            productsTrackingInfo.push({\n                'item_id': product.id,\n                'item_name': product.display_name,\n                'item_category': product.category_name,\n                'currency': product.currency_name,\n                'price': product.price,\n                'quantity': product.quantity,\n            });\n        }\n        if (productsTrackingInfo.length) {\n            this.$el.trigger('add_to_cart_event', productsTrackingInfo);\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport wUtils from \"@website/js/utils\";\n\nexport const cartHandlerMixin = {\n    getRedirectOption() {\n        const html = document.documentElement;\n        this.stayOnPageOption = html.dataset.add2cartRedirect === '1';\n    },\n    getCartHandlerOptions(ev) {\n        this.isBuyNow = ev.currentTarget.classList.contains('o_we_buy_now');\n        const targetSelector = ev.currentTarget.dataset.animationSelector || 'img';\n        this.$itemImgContainer = this.$(ev.currentTarget).closest(`:has(${targetSelector})`);\n    },\n    /**\n     * Used to add product depending on stayOnPageOption value.\n     */\n    addToCart(params) {\n        if (this.isBuyNow) {\n            params.express = true;\n        } else if (this.stayOnPageOption) {\n            return this._addToCartInPage(params);\n        }\n        return wUtils.sendRequest('/shop/cart/update', params);\n    },\n    /**\n     * @private\n     */\n    async _addToCartInPage(params) {\n        const data = await rpc(\"/shop/cart/update_json\", {\n            ...params,\n            display: false,\n            force_create: true,\n        });\n        if (data.cart_quantity && (data.cart_quantity !== parseInt($(\".my_cart_quantity\").text()))) {\n            updateCartNavBar(data);\n        };\n        showCartNotification(this.call.bind(this), data.notification_info);\n        return data;\n    },\n};\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 * Updates both navbar cart\n * @param {Object} data\n */\nfunction updateCartNavBar(data) {\n    sessionStorage.setItem('website_sale_cart_quantity', data.cart_quantity);\n    $(\".my_cart_quantity\")\n        .parents('li.o_wsale_my_cart').removeClass('d-none').end()\n        .toggleClass('d-none', data.cart_quantity === 0)\n        .addClass('o_mycart_zoom_animation').delay(300)\n        .queue(function () {\n            $(this)\n                .toggleClass('fa fa-warning', !data.cart_quantity)\n                .attr('title', data.warning)\n                .text(data.cart_quantity || '')\n                .removeClass('o_mycart_zoom_animation')\n                .dequeue();\n        });\n\n    $(\".js_cart_lines\").first().before(data['website_sale.cart_lines']).end().remove();\n    $(\"#cart_total\").replaceWith(data['website_sale.total']);\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\nfunction showCartNotification(callService, props, options = {}) {\n    // Show the notification about the cart\n    if (props.lines) {\n        callService(\"cartNotificationService\", \"add\", _t(\"Item(s) added to your cart\"), {\n            lines: props.lines,\n            currency_id: props.currency_id,\n            ...options,\n        });\n    }\n    if (props.warning) {\n        callService(\"cartNotificationService\", \"add\", _t(\"Warning\"), {\n            warning: props.warning,\n            ...options,\n        });\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\nexport default {\n    animateClone: animateClone,\n    updateCartNavBar: updateCartNavBar,\n    cartHandlerMixin: cartHandlerMixin,\n    showCartNotification: showCartNotification,\n    showWarning: showWarning,\n};\n", "/** @odoo-module **/\n\nimport { debounce } from \"@web/core/utils/timing\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { cookie } from \"@web/core/browser/cookie\";;\nimport { rpc } from \"@web/core/network/rpc\";\n\npublicWidget.registry.productsRecentlyViewedUpdate = publicWidget.Widget.extend({\n    selector: '#product_detail',\n    events: {\n        'change input.product_id[name=\"product_id\"]': '_onProductChange',\n    },\n    debounceValue: 8000,\n\n    /**\n     * @constructor\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n        this._onProductChange = debounce(this._onProductChange, this.debounceValue);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Debounced method that wait some time before marking the product as viewed.\n     * @private\n     * @param {HTMLInputElement} $input\n     */\n    _updateProductView: function ($input) {\n        var productId = parseInt($input.val());\n        var cookieName = 'seen_product_id_' + productId;\n        if (! parseInt(this.el.dataset.viewTrack, 10)) {\n            return; // Is not tracked\n        }\n        if (cookie.get(cookieName)) {\n            return; // Already tracked in the last 30min\n        }\n        if ($(this.el).find('.js_product.css_not_available').length) {\n            return; // Variant not possible\n        }\n        rpc('/shop/products/recently_viewed_update', {\n            product_id: productId,\n        }).then(function (res) {\n            cookie.set(cookieName, productId, 30 * 60, 'optional');\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Call debounced method when product change to reset timer.\n     * @private\n     * @param {Event} ev\n     */\n    _onProductChange: function (ev) {\n        this._updateProductView($(ev.currentTarget));\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\npublicWidget.registry.websiteSaleTracking = publicWidget.Widget.extend({\n    selector: '.oe_website_sale',\n    events: {\n        'click form[action=\"/shop/cart/update\"] a.a-submit': '_onAddProductIntoCart',\n        'click a[href^=\"/shop/checkout\"]': '_onCheckoutStart',\n        'click a[href^=\"/web/login?redirect\"][href*=\"/shop/checkout\"]': '_onCustomerSignin',\n        'click form[action=\"/shop/confirm_order\"] a.a-submit': '_onOrder',\n        'click form[target=\"_self\"] button[type=submit]': '_onOrderPayment',\n        'view_item_event': '_onViewItem',\n        'add_to_cart_event': '_onAddToCart',\n    },\n\n    /**\n     * @override\n     */\n    start: function () {\n        var self = this;\n\n        // ...\n        const $confirmation = this.$('div.oe_website_sale_tx_status');\n        if ($confirmation.length) {\n            const orderID = $confirmation.data('order-id');\n            const json = $confirmation.data('order-tracking-info');\n            this._vpv('/stats/ecom/order_confirmed/' + orderID);\n            self._trackGA('event', 'purchase', json);\n        }\n\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _trackGA: function () {\n        const websiteGA = window.gtag || function () {};\n        websiteGA.apply(this, arguments);\n    },\n    /**\n     * @private\n     */\n    _vpv: function (page) { //virtual page view\n        this._trackGA('event', 'page_view', {\n            'page_path': page,\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onViewItem(event, productTrackingInfo) {\n        const trackingInfo = {\n            'currency': productTrackingInfo['currency'],\n            'value': productTrackingInfo['price'],\n            'items': [productTrackingInfo],\n        };\n        this._trackGA('event', 'view_item', trackingInfo);\n    },\n\n    /**\n     * @private\n     */\n    _onAddToCart(event, ...productsTrackingInfo) {\n        const trackingInfo = {\n            'currency': productsTrackingInfo[0]['currency'],\n            'value': productsTrackingInfo.reduce((acc, val) => acc + val['price'] * val['quantity'], 0),\n            'items': productsTrackingInfo,\n        };\n        this._trackGA('event', 'add_to_cart', trackingInfo);\n    },\n\n    /**\n     * @private\n     */\n    _onAddProductIntoCart: function () {\n        var productID = this.$('input[name=\"product_id\"]').attr('value');\n        this._vpv('/stats/ecom/product_add_to_cart/' + productID);\n    },\n    /**\n     * @private\n     */\n    _onCheckoutStart: function () {\n        this._vpv('/stats/ecom/customer_checkout');\n    },\n    /**\n     * @private\n     */\n    _onCustomerSignin: function () {\n        this._vpv('/stats/ecom/customer_signin');\n    },\n    /**\n     * @private\n     */\n    _onOrder: function () {\n        if ($('header#top [href=\"/web/login\"]').length) {\n            this._vpv('/stats/ecom/customer_signup');\n        }\n        this._vpv('/stats/ecom/order_checkout');\n    },\n    /**\n     * @private\n     */\n    _onOrderPayment: function () {\n        var method = $('#payment_method input[name=provider]:checked').nextAll('span:first').text();\n        this._vpv('/stats/ecom/order_payment/' + method);\n    },\n});\n\nexport default publicWidget.registry.websiteSaleTracking;\n", "/** @odoo-module **/\n/**\n * This code has been more that widely inspired by the multirange library\n * which can be found on https://github.com/LeaVerou/multirange.\n *\n * The license file can be found in the same folder as this file.\n */\n\n/**\n * The multirange library will display the two values as one range input with\n * two cursors linked by a background. This is to be used with Bootstrap\n * custom-range inputs.\n *\n * There is 2 number inputs on the right and left of the multirange to\n * display and allow quick and precise value modifications. They are\n * initialized with the same value provided to the input (min, max, step).\n *\n * There is 2 events that are added to the input:\n * - oldRangeValue: Triggered when the user clicks on a cursor or on focus\n *                  of the right or left number input.\n * - newRangeValue: Triggered when the user release a cursor or on focus\n *                  out of the right or left number input.\n *\n * The options available for the multirange are :\n * - On range input or as multirange method options:\n *     - min: minimal value of the range. Default: 0.\n *     - max: maximal value of the range. Default: 100.\n *     - step: precision of the range. Default: 1.\n *     - currency: symbol preceding the displayed values. Default: Empty.\n *     - currencyPosition: currency before/after value. Default: \"after\".\n *     - value: the current value of the range. Default: \"0,100\".\n *\n * - As multirange method options only:\n *     - displayCounterInput: if we display the value. Default: true.\n *\n * Initialization of a multiple range input can be done in two ways:\n *\n * Having the inputs with the options as properties and the multiple\n * property set will let the library initialize it just after DOM loaded.\n *\n * <input type=\"range\" multiple=\"multiple\" class=\"custom-range\n * range-with-input\" min=2 max=10 step=0.5 data-currency=\"\u20ac\"\n * data-currency-position=\"before\" value=\"4,8\"/>\n *\n * Providing a HTMLElement and an Object with the desired options.\n *\n * <input id=\"multi\" type=\"range\" class=\"custom-range\"/>\n *\n * multirange(document.querySelector('#multi'), {\n *     min: 2,\n *     max: 10,\n *     step: 0.5,\n *     currency: \"\u20ac\",\n *     currencyPosition: \"before\",\n *     value: \"4,8\"\n *     rangeWithInput: true,\n * });\n */\n\nconst HTMLInputElement = window.HTMLInputElement;\nconst descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, \"value\");\n\nexport class Multirange {\n    constructor(input, options = {}) {\n        const self = this;\n\n        /* Set default and optionnal values */\n        this.input = input;\n        this.rangeWithInput = options.rangeWithInput === true || this.input.classList.contains('range-with-input');\n        const value = options.value || this.input.getAttribute(\"value\");\n        const values = value === null ? [] : value.split(\",\");\n        this.input.min = this.min = options.min || this.input.min || 0;\n        this.input.max = this.max = options.max || this.input.max || 100;\n        this.input.step = this.step = options.step || this.input.step || 1;\n        this.currency = options.currency || this.input.dataset.currency || '';\n        this.currencyPosition = options.currencyPosition || this.input.dataset.currencyPosition || 'after';\n        const inputClasses = [\"multirange\", \"d-inline-block\", \"p-0\", \"align-top\"];\n\n        /* Wrap the input and add its ghost */\n        this.rangeDiv = document.createElement(\"div\");\n        this.rangeDiv.classList.add(\"multirange-wrapper\", \"position-relative\", \"mb-5\");\n        this.countersWrapper = document.createElement(\"small\");\n        this.countersWrapper.classList.add(\"d-flex\", \"justify-content-between\", \"mt-2\");\n        this.rangeDiv.appendChild(this.countersWrapper);\n        this.input.parentNode.insertBefore(this.rangeDiv, this.input.nextSibling);\n        this.rangeDiv.appendChild(this.input);\n        this.ghost = this.input.cloneNode();\n        this.rangeDiv.appendChild(this.ghost);\n\n        this.input.classList.add(\"original\", \"position-absolute\", \"w-100\", \"m-0\", ...inputClasses);\n        this.ghost.classList.add(\"ghost\", \"position-relative\", ...inputClasses);\n        this.input.value = values[0] || this.min;\n        this.ghost.value = values[1] || this.max;\n\n        this.leftCounter = document.createElement(\"span\");\n        this.leftCounter.classList.add(\"multirange-min\", \"position-absolute\", \"opacity-75\", \"opacity-100-hover\", \"mt-1\");\n        this.rightCounter = document.createElement(\"span\");\n        this.rightCounter.classList.add(\"multirange-max\", \"position-absolute\", \"opacity-75\", \"opacity-100-hover\", \"mt-1\", \"end-0\");\n        this.countersWrapper.append(this.leftCounter, this.rightCounter);\n\n        /* Add the counterInput */\n        if (this.rangeWithInput) {\n            this.leftInput = document.createElement(\"input\");\n            this.leftInput.type = \"number\";\n            this.leftInput.classList.add(\"invisible\", \"form-control\", \"form-control-sm\", \"mb-2\", \"mb-lg-1\");\n            this.leftInput.min = this.min;\n            this.leftInput.max = this.max;\n            this.leftInput.step = this.step;\n            this.rightInput = this.leftInput.cloneNode();\n\n            this.leftInput.classList.add(\"multirange-min\", \"invisible\");\n            this.rightInput.classList.add(\"multirange-max\", \"invisible\");\n\n            this.leftCounter.parentNode.appendChild(this.leftInput);\n            this.rightCounter.parentNode.appendChild(this.rightInput);\n        }\n\n        /* Define new properties on range input to link it with ghost, especially for Safari compatibility*/\n        Object.defineProperty(this.input, \"originalValue\", descriptor.get ? descriptor : {\n            get: function () {\n                return this.value;\n            },\n            set: function (v) {\n                this.value = v;\n            }\n        });\n\n        Object.defineProperties(this.input, {\n            valueLow: {\n                get: function () {\n                    return Math.min(this.originalValue, self.ghost.value);\n                },\n                set: function (v) {\n                    this.originalValue = v;\n                },\n                enumerable: true\n            },\n            valueHigh: {\n                get: function () {\n                    return Math.max(this.originalValue, self.ghost.value);\n                },\n                set: function (v) {\n                    self.ghost.value = v;\n                },\n                enumerable: true\n            }\n        });\n\n        if (descriptor.get) {\n            Object.defineProperty(this.input, \"value\", {\n                get: function () {\n                    return this.valueLow + \",\" + this.valueHigh;\n                },\n                set: function (v) {\n                    const values = v.split(\",\");\n                    this.valueLow = values[0];\n                    this.valueHigh = values[1];\n                    this.update();\n                },\n                enumerable: true\n            });\n        }\n\n        if (typeof this.input.oninput === \"function\") {\n            this.ghost.oninput = this.input.oninput.bind(this.input);\n        }\n\n        this.input.addEventListener(\"input\", this.update.bind(this));\n        this.ghost.addEventListener(\"input\", this.update.bind(this));\n\n        this.input.addEventListener(\"touchstart\", this.saveOldValues.bind(this));\n        this.ghost.addEventListener(\"touchstart\", this.saveOldValues.bind(this));\n        this.input.addEventListener(\"mousedown\", this.saveOldValues.bind(this));\n        this.ghost.addEventListener(\"mousedown\", this.saveOldValues.bind(this));\n\n        this.input.addEventListener(\"touchend\", this.dispatchNewValueEvent.bind(this));\n        this.ghost.addEventListener(\"touchend\", this.dispatchNewValueEvent.bind(this));\n        this.input.addEventListener(\"mouseup\", this.dispatchNewValueEvent.bind(this));\n        this.ghost.addEventListener(\"mouseup\", this.dispatchNewValueEvent.bind(this));\n\n        if (this.rangeWithInput) {\n            this.leftCounter.addEventListener(\"click\", this.counterInputSwitch.bind(this));\n            this.rightCounter.addEventListener(\"click\", this.counterInputSwitch.bind(this));\n\n            this.leftInput.addEventListener(\"blur\", this.counterInputSwitch.bind(this));\n            this.rightInput.addEventListener(\"blur\", this.counterInputSwitch.bind(this));\n\n            this.leftInput.addEventListener(\"keypress\", this.elementBlurOnEnter.bind(this));\n            this.rightInput.addEventListener(\"keypress\", this.elementBlurOnEnter.bind(this));\n\n            this.leftInput.addEventListener(\"focus\", this.selectAllFocus.bind(this));\n            this.rightInput.addEventListener(\"focus\", this.selectAllFocus.bind(this));\n        }\n        this.update();\n        $(this.rangeDiv).addClass('visible');\n\n    }\n\n    update() {\n        const low = 100 * (this.input.valueLow - this.min) / (this.max - this.min);\n        const high = 100 * (this.input.valueHigh - this.min) / (this.max - this.min);\n\n        this.rangeDiv.style.setProperty(\"--low\", low + '%');\n        this.rangeDiv.style.setProperty(\"--high\", high + '%');\n        this.counterInputUpdate();\n    }\n\n    counterInputUpdate() {\n        if (this.rangeWithInput) {\n            this.leftCounter.innerText = this.formatNumber(this.input.valueLow);\n            this.rightCounter.innerText = this.formatNumber(this.input.valueHigh);\n            this.leftInput.value = this.input.valueLow;\n            this.rightInput.value = this.input.valueHigh;\n        }\n    }\n\n    counterInputSwitch(ev) {\n        let counter = this.rightCounter;\n        let input = this.rightInput;\n        if (ev.currentTarget.classList.contains('multirange-min')) {\n            counter = this.leftCounter;\n            input = this.leftInput;\n        }\n\n        if (counter.classList.contains(\"invisible\")) {\n            this.input.valueLow = this.leftInput.value;\n            this.input.valueHigh = this.rightInput.value;\n            this.dispatchNewValueEvent();\n            this.update();\n            counter.classList.remove(\"invisible\");\n            input.classList.add(\"invisible\");\n        } else {\n            counter.classList.add(\"invisible\");\n            input.classList.remove(\"invisible\");\n            this.saveOldValues();\n            // Hack because firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1057858\n            window.setTimeout(function () {\n                input.focus();\n            }, 1);\n        }\n    }\n\n    elementBlurOnEnter(ev) {\n        if (ev.key === \"Enter\") {\n            ev.currentTarget.blur();\n        }\n    }\n\n    selectAllFocus(ev) {\n        ev.currentTarget.select();\n    }\n\n    dispatchNewValueEvent() {\n        if (this._previousMaxPrice !== this.input.valueHigh || this._previousMinPrice !== this.input.valueLow) {\n            this.input.dispatchEvent(new CustomEvent(\"newRangeValue\", {\n                bubbles: true,\n            }));\n        }\n    }\n\n    saveOldValues() {\n        this._previousMinPrice = this.input.valueLow;\n        this._previousMaxPrice = this.input.valueHigh;\n    }\n\n    formatNumber(number) {\n        const language = document.querySelector(\"html\").getAttribute(\"lang\");\n        const locale = (() => {\n            switch (language) {\n                case \"sr@latin\":\n                    return \"sr-Latn\";\n                case \"sr@Cyrl\":\n                    return \"sr-Cyrl\";\n                default:\n                    return language.replaceAll(\"_\", \"-\");\n            }\n        })();\n        let formatedNumber = number.toLocaleString(locale, {\n            minimumFractionDigits: 2,\n            maximumFractionDigits: 2,\n        });\n        if (this.currency.length) {\n            if (this.currencyPosition === 'after') {\n                formatedNumber = formatedNumber + ' ' + this.currency;\n            } else {\n                formatedNumber = this.currency + ' ' + formatedNumber;\n            }\n        }\n        return formatedNumber;\n    }\n}\n\nexport function multirange(input, options) {\n    if (input.classList.contains('multirange')) {\n        return;\n    }\n    new Multirange(input, options);\n}\n\nexport default {\n    Multirange: Multirange,\n    init: multirange,\n};\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport multirange from \"@website/../lib/multirange/multirange_custom\";\n\npublicWidget.registry.WebsiteMultirangeInputs = publicWidget.Widget.extend({\n    selector: 'input[type=range][multiple]:not(.multirange)',\n\n    /**\n     * @override\n     */\n    start() {\n        return this._super.apply(this, arguments).then(() => {\n            multirange.init(this.el);\n        });\n    },\n});\n", "/** @odoo-module **/\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { onRendered, useRef, useEffect, useState } from \"@odoo/owl\";\n\nconst ZOOM_STEP = 0.1;\n\nexport class ProductImageViewer extends Dialog {\n    static template = \"website_sale.ProductImageViewer\";\n    static props = {\n        ...Dialog.props,\n        images: { type: NodeList, required: true },\n        selectedImageIdx: { type: Number, optional: true },\n        close: Function,\n    };\n\n    setup() {\n        super.setup();\n        this.imageContainerRef = useRef(\"imageContainer\");\n        this.images = [...this.props.images].map(image => {\n            return {\n                src: image.dataset.zoomImage || image.src,\n                thumbnailSrc: image.src.replace('/image_1024/', '/image_256/'),\n            };\n        });\n        this.state = useState({\n            selectedImageIdx: this.props.selectedImageIdx || 0,\n            imageScale: 1,\n        });\n        this.isDragging = false;\n        this.dragStartPos = { x: 0, y: 0 };\n        // Doing a full render for the translate is too slow.\n        this.imageTranslate = { x: 0, y: 0 };\n        useHotkey(\"arrowleft\", this.previousImage.bind(this));\n        useHotkey(\"arrowright\", this.nextImage.bind(this));\n        useHotkey(\"r\", () => {\n            this.imageTranslate = { x: 0, y: 0 };\n            this.isDragging = false;\n            this.state.imageScale = 1;\n            this.updateImage();\n        });\n\n        // Not using a t-on-click on purpose because we want to be able to cancel the drag\n        // when we go outside of the window.\n        useEffect(\n            (document) => {\n                const onGlobalClick = this.onGlobalClick.bind(this);\n                document.addEventListener(\"click\", onGlobalClick);\n                return () => {document.removeEventListener(\"click\", onGlobalClick)};\n            },\n            () => [document],\n        );\n        // For some reason the styling does not always update properly.\n        onRendered(() => {\n            this.updateImage();\n        })\n    }\n\n    get selectedImage() {\n        return this.images[this.state.selectedImageIdx];\n    }\n\n    set selectedImage(image) {\n        this.state.imageScale = 1;\n        this.imageTranslate = { x: 0, y: 0 };\n        this.state.selectedImageIdx = this.images.indexOf(image);\n    }\n\n    get imageStyle() {\n        return `transform:\n            scale3d(${this.state.imageScale}, ${this.state.imageScale}, 1);\n        `;\n    }\n\n    get imageContainerStyle() {\n        return `transform: translate(${this.imageTranslate.x}px, ${this.imageTranslate.y}px);`;\n    }\n\n    previousImage() {\n        this.selectedImage = this.images[(this.state.selectedImageIdx - 1 + this.images.length) % this.images.length];\n    }\n\n    nextImage() {\n        this.selectedImage = this.images[(this.state.selectedImageIdx + 1) % this.images.length];\n    }\n\n    updateImage() {\n        if (!this.imageContainerRef || !this.imageContainerRef.el) {\n            return;\n        }\n        this.imageContainerRef.el.style = this.imageContainerStyle;\n    }\n\n    onGlobalClick(ev) {\n        if (ev.target.tagName === \"IMG\") {\n            // Only zoom if the image did not move\n            if (this.dragStartPos.clientX === ev.clientX && this.dragStartPos.clientY === ev.clientY) {\n                this.zoomIn(ZOOM_STEP * 3);\n            }\n        }\n        if (ev.target.classList.contains('o_wsale_image_viewer_void') && !this.isDragging) {\n            ev.stopPropagation();\n            ev.preventDefault();\n            this.data.close();\n        } else {\n            this.isDragging = false;\n        }\n    }\n\n    zoomIn(step=undefined) {\n        this.state.imageScale += step || ZOOM_STEP;\n    }\n\n    zoomOut(step=undefined) {\n        this.state.imageScale = Math.max(0.5, this.state.imageScale - (step || ZOOM_STEP));\n    }\n\n    onWheelImage(ev) {\n        if (ev.deltaY > 0) {\n            this.zoomOut();\n        } else {\n            this.zoomIn();\n        }\n    }\n\n    onMousedownImage(ev) {\n        this.isDragging = true;\n        this.dragStartPos = {\n            x: ev.clientX - this.imageTranslate.x,\n            y: ev.clientY - this.imageTranslate.y,\n            clientX: ev.clientX,\n            clientY: ev.clientY,\n        };\n    }\n\n    onGlobalMousemove(ev) {\n        if (!this.isDragging) {\n            return;\n        }\n        this.imageTranslate.x = ev.clientX - this.dragStartPos.x;\n        this.imageTranslate.y = ev.clientY - this.dragStartPos.y;\n        this.updateImage();\n    }\n}\ndelete ProductImageViewer.props.slots;\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { debounce as debounceFn } from \"@web/core/utils/timing\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { formatCurrency } from \"@web/core/currency\";\nimport { rpc } from \"@web/core/network/rpc\";\n\n// Widget responsible for openingn the modal (giving out the sale order id)\n\npublicWidget.registry.SaleOrderPortalReorderWidget = publicWidget.Widget.extend({\n    selector: \".o_portal_sidebar\",\n    events: {\n        \"click .o_wsale_reorder_button\": \"_onReorder\",\n    },\n\n    _onReorder(ev) {\n        const orderId = parseInt(ev.currentTarget.dataset.saleOrderId);\n        const urlSearchParams = new URLSearchParams(window.location.search);\n        if (!orderId || !urlSearchParams.has(\"access_token\")) {\n            return;\n        }\n        // Open the modal\n        this.call(\"dialog\", \"add\", ReorderDialog, {\n            orderId: orderId,\n            accessToken: urlSearchParams.get(\"access_token\"),\n        });\n    },\n});\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Component, onWillStart } from \"@odoo/owl\";\n\n// Reorder Dialog\n\nexport class ReorderConfirmationDialog extends ConfirmationDialog {\n    static template = \"website_sale.ReorderConfirmationDialog\";\n}\n\nexport class ReorderDialog extends Component {\n    static template = \"website_sale.ReorderModal\";\n    static props = {\n        close: Function,\n        orderId: Number,\n        accessToken: String,\n    };\n    static components = {\n        Dialog,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.dialogService = useService(\"dialog\");\n        this.formatCurrency = formatCurrency;\n\n        onWillStart(this.onWillStartHandler.bind(this));\n    }\n\n    async onWillStartHandler() {\n        // Cart Qty should not change while the dialog is opened.\n        this.cartQty = parseInt(sessionStorage.getItem(\"website_sale_cart_quantity\"));\n        if (!this.cartQty) {\n            this.cartQty = await rpc(\"/shop/cart/quantity\");\n        }\n        // Get required information about the order\n        this.content = await rpc(\"/my/orders/reorder_modal_content\", {\n            order_id: this.props.orderId,\n            access_token: this.props.accessToken,\n        });\n        // Get required information about each products\n        for (const product of this.content.products) {\n            product.debouncedLoadProductCombinationInfo = debounceFn(() => {\n                this.loadProductCombinationInfo(product).then(this.render.bind(this));\n            }, 200);\n        }\n    }\n\n    get total() {\n        return this.content.products.reduce((total, product) => {\n            if (product.add_to_cart_allowed) {\n                total += product.combinationInfo.price * product.qty;\n            }\n            return total;\n        }, 0);\n    }\n\n    get hasBuyableProducts() {\n        return this.content.products.some((product) => product.add_to_cart_allowed);\n    }\n\n    async loadProductCombinationInfo(product) {\n        product.combinationInfo = await rpc(\"/website_sale/get_combination_info\", {\n            product_template_id: product.product_template_id,\n            product_id: product.product_id,\n            combination: product.combination,\n            add_qty: product.qty,\n            context: {\n                website_sale_no_images: true,\n            },\n        });\n    }\n\n    getWarningForProduct(product) {\n        if (!product.add_to_cart_allowed) {\n            return _t(\"This product is not available for purchase.\");\n        }\n        return false;\n    }\n\n    changeProductQty(product, newQty) {\n        const productNewQty = Math.max(0, newQty);\n        const qtyChanged = productNewQty !== product.qty;\n        product.qty = productNewQty;\n        this.render(true);\n        if (!qtyChanged) {\n            return;\n        }\n        product.debouncedLoadProductCombinationInfo();\n    }\n\n    onChangeProductQtyInput(ev, product) {\n        const newQty = parseFloat(ev.target.value) || product.qty;\n        this.changeProductQty(product, newQty);\n    }\n\n    async confirmReorder(ev) {\n        if (this.confirmed) {\n            return;\n        }\n        this.confirmed = true;\n        const onConfirm = async () => {\n            await this.addProductsToCart();\n            window.location = \"/shop/cart\";\n        };\n        if (this.cartQty) {\n            // Open confirmation modal\n            this.dialogService.add(ReorderConfirmationDialog, {\n                body: _t(\"Do you wish to clear your cart before adding products to it?\"),\n                confirm: async () => {\n                    await rpc(\"/shop/cart/clear\");\n                    await onConfirm();\n                },\n                cancel: onConfirm,\n            });\n        } else {\n            await onConfirm();\n        }\n    }\n\n    async addProductsToCart() {\n        for (const product of this.content.products) {\n            if (!product.add_to_cart_allowed) {\n                continue;\n            }\n            await rpc(\"/shop/cart/update_json\", {\n                product_id: product.product_id,\n                add_qty: product.qty,\n                no_variant_attribute_value_ids: product.no_variant_attribute_value_ids,\n                product_custom_attribute_values: JSON.stringify(product.product_custom_attribute_values),\n                display: false,\n            });\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\nimport { formatCurrency } from \"@web/core/currency\";\n\nexport class AddToCartNotification extends Component {\n    static template = \"website_sale.addToCartNotification\";\n    static props = {\n        lines: {\n            type: Array,\n            element: {\n                type: Object,\n                shape: {\n                    id: Number,\n                    linked_line_id: { type: Number, optional: true },\n                    image_url: String,\n                    quantity: Number,\n                    name: String,\n                    description: { type: String, optional: true },\n                    line_price_total: Number,\n                },\n            },\n        },\n        currency_id: Number,\n    }\n\n    /**\n     * Return the lines which aren't linked to other lines.\n     *\n     * @return {Object[]} The lines which aren't linked to other lines.\n     */\n    get mainLines() {\n        return this.props.lines.filter(line => !line.linked_line_id);\n    }\n\n    /**\n     * Return the lines linked to the provided line id.\n     *\n     * @param {Number} lineId The id of the line whose linked lines to return.\n     * @return {Object[]} The lines which aren't linked to other lines.\n     */\n    getLinkedLines(lineId) {\n        return this.props.lines.filter(line => line.linked_line_id === lineId);\n    }\n\n    /**\n     * Return the price, in the format of the sale order currency.\n     *\n     * @param {Object} line - The line element for which to return the formatted price.\n     * @return {String} - The price, in the format of the sale order currency.\n     */\n    getFormattedPrice(line) {\n        const linkedLines = this.getLinkedLines(line.id);\n        const price = linkedLines.length\n            ? linkedLines.reduce((price, linkedLine) => price + linkedLine.line_price_total, 0)\n            : line.line_price_total;\n        return formatCurrency(price, this.props.currency_id);\n    }\n\n    /**\n     * Return the product summary based on the line information.\n     *\n     * The product summary is computed based on the line quantity and name, separated by the symbol\n     * 'x' (e.g.: 1 x Chair Floor Protection).\n     *\n     * @param {Object} line - The line element for which to return the product summary.\n     * @return {String} - The product summary.\n     */\n    getProductSummary(line) {\n        return line.quantity + \" x \" + line.name;\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\nimport { AddToCartNotification } from \"../add_to_cart_notification/add_to_cart_notification\";\nimport { WarningNotification } from \"../warning_notification/warning_notification\";\n\nexport class CartNotification extends Component {\n    static components = { AddToCartNotification, WarningNotification };\n    static template = \"website_sale.cartNotification\";\n    static props = {\n        message: [String, { toString: Function }],\n        warning: {type : [String, { toString: Function }],optional: true},\n        lines: {\n            type: Array,\n            optional: true,\n            element: {\n                type: Object,\n                shape: {\n                    id: Number,\n                    linked_line_id: { type: Number, optional: true },\n                    image_url: String,\n                    quantity: Number,\n                    name: String,\n                    description: { type: String, optional: true },\n                    line_price_total: Number,\n                },\n            },\n        },\n        currency_id: Number,\n        className: String,\n        close: Function,\n        refresh: Function,\n        freeze: Function,\n    }\n\n    /**\n     * Get the top position (in px) of the notification based on the navbar height.\n     *\n     * This prevents the notification from being shown in front of the navbar.\n     */\n    get positionOffset() {\n        return (document.querySelector('header.o_top_fixed_element')?.offsetHeight || 0) + 'px';\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\n\nexport class WarningNotification extends Component {\n    static template = \"website_sale.warningNotification\";\n    static props = {\n        warning: [String, { toString: Function }],\n    }\n}\n", "/** @odoo-module **/\n\nimport { xml } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { notificationService } from \"@web/core/notifications/notification_service\";\nimport { NotificationContainer } from \"@web/core/notifications/notification_container\";\nimport { CartNotification } from \"@website_sale/js/notification/cart_notification/cart_notification\";\n\n\nexport class CartNotificationContainer extends NotificationContainer {\n    static components = {\n        ...NotificationContainer.components,\n        Notification: CartNotification,\n    }\n    static template = xml`\n    <div class=\"position-absolute w-100 h-100 top-0 pe-none\">\n        <div class=\"d-flex flex-column container align-items-end\">\n            <t t-foreach=\"notifications\" t-as=\"notification\" t-key=\"notification\">\n                <Transition leaveDuration=\"0\" name=\"'o_notification_fade'\" t-slot-scope=\"transition\">\n                    <Notification t-props=\"notification_value.props\" className=\"(notification_value.props.className || '') + ' ' + transition.className\"/>\n                </Transition>\n            </t>\n        </div>\n    </div>`;\n}\n\nexport const cartNotificationService = {\n    ...notificationService,\n    notificationContainer: CartNotificationContainer,\n}\n\nregistry.category(\"services\").add(\"cartNotificationService\", cartNotificationService);\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\nimport { formatCurrency } from \"@web/core/currency\";\n\nexport class BadgeExtraPrice extends Component {\n    static template = \"sale.BadgeExtraPrice\";\n    static props = {\n        price: Number,\n        currencyId: Number,\n    };\n\n    /**\n     * Return the price, in the format of the given currency.\n     *\n     * @return {String} - The price, in the format of the given currency.\n     */\n    getFormattedPrice() {\n        return formatCurrency( Math.abs(this.props.price), this.props.currencyId);\n    }\n}\n", "import { _t } from '@web/core/l10n/translation';\nimport { Dialog } from '@web/core/dialog/dialog';\nimport { formatCurrency } from '@web/core/currency';\nimport { rpc } from '@web/core/network/rpc';\nimport { useService } from '@web/core/utils/hooks';\nimport { Component, useState, useSubEnv } from '@odoo/owl';\nimport { ProductCard } from '../product_card/product_card';\nimport { ProductCombo } from '../models/product_combo';\nimport { ProductTemplateAttributeLine } from '../models/product_template_attribute_line';\nimport {\n    ProductConfiguratorDialog\n} from '../product_configurator_dialog/product_configurator_dialog';\nimport { QuantityButtons } from '../quantity_buttons/quantity_buttons';\n\nexport class ComboConfiguratorDialog extends Component {\n    static template = 'sale.ComboConfiguratorDialog';\n    static components = { Dialog, ProductCard, QuantityButtons };\n    static props = {\n        product_tmpl_id: Number,\n        display_name: String,\n        quantity: Number,\n        price: Number,\n        combos: { type: Array, element: ProductCombo },\n        currency_id: Number,\n        company_id: { type: Number, optional: true },\n        pricelist_id: { type: Number, optional: true },\n        date: String,\n        price_info: { type: String, optional: true },\n        edit: { type: Boolean, optional: true },\n        save: Function,\n        discard: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.dialog = useService('dialog');\n        this.env.dialogData.dismiss = !this.props.edit && this.props.discard.bind(this);\n        this.state = useState({\n            // Maps combo ids to selected combo items.\n            // Note that selected combo items can be modified (i.e. their `no_variant` PTAVs can be\n            // updated), so this map stores deep copies to avoid modifying the props.\n            selectedComboItems: new Map(),\n            quantity: this.props.quantity,\n            basePrice: this.props.price,\n            isLoading: false,\n        });\n        if (this.props.edit) this._initSelectedComboItems();\n        this._selectSingleComboItems();\n        this.getPriceUrl = '/sale/combo_configurator/get_price';\n        useSubEnv({ currency: { id: this.props.currency_id } });\n    }\n\n    /**\n     * Select the provided combo item, and open the product configurator iff the combo item's\n     * product is configurable.\n     *\n     * @param {Number} comboId The id of the combo to which the combo item belongs.\n     * @param {ProductComboItem} comboItem The combo item to select.\n     */\n    async selectComboItem(comboId, comboItem) {\n        // Use up-to-date selected PTAVs and custom values to populate the product configurator.\n        comboItem = this.getSelectedOrProvidedComboItem(comboId, comboItem);\n        let product = comboItem.product;\n        if (product.hasNoVariantPtals) {\n            // TODO(loti): replace content instead of stacking dialogs?\n            this.dialog.add(ProductConfiguratorDialog, {\n                productTemplateId: product.product_tmpl_id,\n                ptavIds: product.selectedPtavIds,\n                customPtavs: product.selectedCustomPtavs,\n                quantity: 1,\n                companyId: this.props.company_id,\n                pricelistId: this.props.pricelist_id,\n                currencyId: this.props.currency_id,\n                soDate: this.props.date,\n                edit: true, // TODO(loti): this \"disables\" optional products. Rename variable for clarity?\n                options: { canChangeVariant: false, showQuantityAndPrice: false },\n                save: async configuredProduct => {\n                    const selectedComboItem = comboItem.deepCopy();\n                    selectedComboItem.product.ptals = configuredProduct.attribute_lines.map(\n                        ProductTemplateAttributeLine.fromProductConfiguratorPtal\n                    );\n                    this.state.selectedComboItems.set(comboId, selectedComboItem);\n                },\n                discard: () => {},\n                ...this._getAdditionalDialogProps(),\n            });\n        } else {\n            this.state.selectedComboItems.set(comboId, comboItem.deepCopy());\n        }\n    }\n\n    /**\n     * Sets the quantity of this combo product.\n     *\n     * @param {Number} quantity The new quantity of this combo product.\n     */\n    async setQuantity(quantity) {\n        this.state.quantity = quantity;\n        this.state.basePrice = await rpc(this.getPriceUrl, {\n            product_tmpl_id: this.props.product_tmpl_id,\n            currency_id: this.props.currency_id,\n            quantity: quantity,\n            date: this.props.date,\n            company_id: this.props.company_id,\n            pricelist_id: this.props.pricelist_id,\n            ...this._getAdditionalRpcParams(),\n        });\n    }\n\n    /**\n     * Return the selected or provided combo item.\n     *\n     * If the provided combo item was already selected, then it may contain stale data (i.e.\n     * selected PTAVs, custom values), and we should rely on the data in `state.selectedComboItems`\n     * instead. Otherwise, the data in the provided combo item is up-to-date and can be used.\n     *\n     * @param {Number} comboId The id of the combo to which the combo item belongs.\n     * @param {ProductComboItem} comboItem The provided combo item.\n     * @return {ProductComboItem} The selected or provided combo item.\n     */\n    getSelectedOrProvidedComboItem(comboId, comboItem) {\n        const selectedComboItem = this.state.selectedComboItems.get(comboId);\n        const isComboItemAlreadySelected = selectedComboItem?.id === comboItem.id;\n        return isComboItemAlreadySelected ? selectedComboItem : comboItem;\n    }\n\n    get totalMessage() {\n        return _t(\"Total: %s\", this.formattedTotalPrice);\n    }\n\n    /**\n     * Return the total price, formatted using the provided currency.\n     *\n     * The total price is the sum of:\n     * - The combo product's price,\n     * - The selected combo items' extra price,\n     * - The selected `no_variant` attributes' extra price.\n     *\n     * @return {String} The formatted total price.\n     */\n    get formattedTotalPrice() {\n        return formatCurrency(this._totalPrice, this.props.currency_id);\n    }\n\n    /**\n     * Check whether a combo item has been selected for each combo.\n     *\n     * @return {Boolean} Whether a combo item has been selected for each combo.\n     */\n    get areAllCombosSelected() {\n        return this.state.selectedComboItems.size === this.props.combos.length;\n    }\n\n    async confirm(options) {\n        this.state.isLoading = true;\n        await this.props.save(this._comboProductData, this._selectedComboItems, options).finally(\n            () => this.state.isLoading = false\n        )\n        this.props.close();\n    }\n\n    cancel() {\n        if (!this.props.edit) {\n            this.props.discard();\n        }\n        this.props.close();\n    }\n\n    /**\n     * Initialize the selected combo item in each combo.\n     */\n    _initSelectedComboItems() {\n        for (const combo of this.props.combos) {\n            const comboItem = combo.selectedComboItem;\n            if (comboItem) {\n                this.state.selectedComboItems.set(combo.id, comboItem.deepCopy());\n            }\n        }\n    }\n\n    /**\n     * Automatically select the single combo item in each combo that has a single, non-configurable\n     * combo item.\n     */\n    _selectSingleComboItems() {\n        for (const combo of this.props.combos) {\n            const comboItem = combo.combo_items[0];\n            if (combo.combo_items.length === 1 && !comboItem.product.hasNoVariantPtals) {\n                this.state.selectedComboItems.set(combo.id, comboItem.deepCopy());\n            }\n        }\n    }\n\n    get _totalPrice() {\n        const extraPrice = Array.from(\n            this.state.selectedComboItems.values(),\n            comboItem => comboItem.extra_price + comboItem.product.selectedNoVariantPtavsPriceExtra,\n        ).reduce((price, comboItemExtraPrice) => price + comboItemExtraPrice, 0);\n        return this.state.quantity * (this.state.basePrice + extraPrice);\n    }\n\n    /**\n     * Return data about the combo product.\n     *\n     * @return {Object} Data about the combo product.\n     */\n    get _comboProductData() {\n        return { 'quantity': this.state.quantity };\n    }\n\n    /**\n     * Return the selected combo items.\n     *\n     * @return {ProductComboItem[]} The selected combo items.\n     */\n    get _selectedComboItems() {\n        return Array.from(this.state.selectedComboItems.values());\n    }\n\n    /**\n     * Hook to append additional RPC params in overriding modules.\n     *\n     * @return {Object} The additional RPC params.\n     */\n    _getAdditionalRpcParams() {\n        return {};\n    }\n\n    /**\n     * Hook to append additional props in overriding modules.\n     *\n     * @return {Object} The additional props.\n     */\n    _getAdditionalDialogProps() {\n        return {};\n    }\n}\n", "import { ProductComboItem } from './product_combo_item';\n\nexport class ProductCombo {\n    /**\n     * @param {number} id\n     * @param {string} name\n     * @param {ProductComboItem[]|object[]} combo_items\n     */\n    constructor({id, name, combo_items}) {\n        this.id = id;\n        this.name = name;\n        this.combo_items = combo_items.map(item => new ProductComboItem(item));\n    }\n\n    /**\n     * Return the selected combo item, if any.\n     *\n     * @return {ProductComboItem|undefined} The selected combo item, if any.\n     */\n    get selectedComboItem() {\n        return this.combo_items.find(item => item.is_selected);\n    }\n}\n", "import { ProductProduct } from './product_product';\n\nexport class ProductComboItem {\n    /**\n     * @param {number} id\n     * @param {number} extra_price\n     * @param {boolean} is_selected\n     * @param {ProductProduct|object} product\n     */\n    constructor({id, extra_price, is_selected, product}) {\n        this.id = id;\n        this.extra_price = extra_price;\n        this.is_selected = is_selected;\n        this.product = new ProductProduct(product);\n    }\n\n    /**\n     * Return a deep copy of this combo item.\n     *\n     * @return {ProductComboItem} A deep copy of this combo item.\n     */\n    deepCopy() {\n        return new ProductComboItem(JSON.parse(JSON.stringify(this)));\n    }\n}\n", "import { ProductTemplateAttributeLine } from './product_template_attribute_line';\n\nexport class ProductProduct {\n    /**\n     * The instance is initialized in `setup` to allow patching, as constructors can't be patched.\n     */\n    constructor(...args) {\n        this.setup(...args);\n    }\n\n    /**\n     * @param {number} id\n     * @param {number} product_tmpl_id\n     * @param {string} display_name\n     * @param {ProductTemplateAttributeLine[]|object[]} ptals\n     */\n    setup({id, product_tmpl_id, display_name, ptals}) {\n        this.id = id;\n        this.product_tmpl_id = product_tmpl_id;\n        this.display_name = display_name;\n        this.ptals = ptals.map(ptal => new ProductTemplateAttributeLine(ptal));\n    }\n\n    /**\n     * Return the `no_variant` PTALs.\n     *\n     * @return {ProductTemplateAttributeLine[]} The `no_variant` PTALs.\n     */\n    get noVariantPtals() {\n        return this.ptals.filter(ptal => ptal.create_variant === 'no_variant');\n    }\n\n    /**\n     * Check whether this product has `no_variant` PTALs.\n     *\n     * @return {Boolean} Whether this product has `no_variant` PTALs.\n     */\n    get hasNoVariantPtals() {\n        return this.noVariantPtals.length > 0;\n    }\n\n    /**\n     * Return the extra price of the selected `no_variant` PTAVs.\n     *\n     * @return {Number} The extra price of the selected `no_variant` PTAVs.\n     */\n    get selectedNoVariantPtavsPriceExtra() {\n        return this.noVariantPtals.reduce((price, ptal) => price + ptal.selectedPtavsPriceExtra, 0);\n    }\n\n    /**\n     * Return the selected PTAV ids.\n     *\n     * @return {Number[]} The selected PTAV ids.\n     */\n    get selectedPtavIds() {\n        return this.ptals.flatMap(ptal => ptal.selected_ptavs).map(ptav => ptav.id);\n    }\n\n    /**\n     * Return the selected `no_variant` PTAV ids.\n     *\n     * @return {Number[]} The selected `no_variant` PTAV ids.\n     */\n    get selectedNoVariantPtavIds() {\n        return this.noVariantPtals.flatMap(ptal => ptal.selected_ptavs).map(ptav => ptav.id);\n    }\n\n    /**\n     * Return the selected custom PTAVs.\n     *\n     * @return {{id: Number, value: String}[]} The selected custom PTAVs.\n     */\n    get selectedCustomPtavs() {\n        return this.ptals.filter(ptal => ptal.hasSelectedCustomPtav).flatMap(\n            ptal => ptal.selected_ptavs\n        ).map(ptav => ({\n            'id': ptav.id,\n            'value': ptav.custom_value,\n        }));\n    }\n}\n", "import { ProductTemplateAttributeValue } from './product_template_attribute_value';\n\nexport class ProductTemplateAttributeLine {\n    /**\n     * @param {number} id\n     * @param {string} name\n     * @param {'always'|'dynamic'|'no_variant'} create_variant\n     * @param {ProductTemplateAttributeValue[]|object[]} selected_ptavs\n     */\n    constructor({id, name, create_variant, selected_ptavs}) {\n        this.id = id;\n        this.name = name;\n        this.create_variant = create_variant;\n        this.selected_ptavs = selected_ptavs.map(ptav => new ProductTemplateAttributeValue(ptav));\n    }\n\n    /**\n     * Construct a ProductTemplateAttributeLine from the provided \"product configurator\"-shaped\n     * PTAL.\n     *\n     * @param productConfiguratorPtal The \"product configurator\"-shaped PTAL.\n     * @return {ProductTemplateAttributeLine} The corresponding ProductTemplateAttributeLine.\n     */\n    static fromProductConfiguratorPtal(productConfiguratorPtal) {\n        const selectedPtavIds = new Set(productConfiguratorPtal.selected_attribute_value_ids);\n        const selectedPtavs = productConfiguratorPtal.attribute_values\n            .filter(ptav => selectedPtavIds.has(ptav.id))\n            .map(ptav => new ProductTemplateAttributeValue({\n                id: ptav.id,\n                name: ptav.name,\n                price_extra: ptav.price_extra,\n                custom_value: productConfiguratorPtal.customValue,\n            }));\n        return new ProductTemplateAttributeLine({\n            id: productConfiguratorPtal.id,\n            name: productConfiguratorPtal.attribute.name,\n            create_variant: productConfiguratorPtal.create_variant,\n            selected_ptavs: selectedPtavs,\n        });\n    }\n\n    /**\n     * Return the extra price of the selected PTAVs.\n     *\n     * @return {Number} The extra price of the selected PTAVs.\n     */\n    get selectedPtavsPriceExtra() {\n        return this.selected_ptavs.reduce((price, ptav) => price + ptav.price_extra, 0);\n    }\n\n    /**\n     * Check whether this PTAL has selected custom PTAVs.\n     *\n     * @return {Boolean} Whether this PTAL has selected custom PTAVs.\n     */\n    get hasSelectedCustomPtav() {\n        return this.selected_ptavs.some(ptav => ptav.custom_value);\n    }\n\n    /**\n     * Return the display name of this PTAL.\n     *\n     * @return {String} The display name of this PTAL.\n     */\n    get ptalDisplayName() {\n        const selectedPtavNames = this.selected_ptavs.map(ptav => ptav.name).join(', ');\n        let ptalDisplayName = `${this.name}: ${selectedPtavNames}`;\n        if (this.hasSelectedCustomPtav) {\n            ptalDisplayName += ` (${this.selected_ptavs[0].custom_value})`;\n        }\n        return ptalDisplayName;\n    }\n}\n", "export class ProductTemplateAttributeValue {\n    /**\n     * @param {number} id\n     * @param {string} name\n     * @param {number} price_extra\n     * @param {string|undefined} custom_value\n     */\n    constructor({id, name, price_extra, custom_value}) {\n        this.id = id;\n        this.name = name;\n        this.price_extra = price_extra;\n        this.custom_value = custom_value;\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { formatCurrency } from \"@web/core/currency\";\nimport {\n    ProductTemplateAttributeLine as PTAL\n} from \"../product_template_attribute_line/product_template_attribute_line\";\nimport { QuantityButtons } from '../quantity_buttons/quantity_buttons';\n\nexport class Product extends Component {\n    static components = { PTAL, QuantityButtons };\n    static template = \"sale.Product\";\n    static props = {\n        id: { type: [Number, {value: false}], optional: true },\n        product_tmpl_id: Number,\n        display_name: String,\n        description_sale: [Boolean, String], // backend sends 'false' when there is no description\n        price: Number,\n        quantity: Number,\n        attribute_lines: Object,\n        optional: Boolean,\n        imageURL: { type: String, optional: true },\n        archived_combinations: Array,\n        exclusions: Object,\n        parent_exclusions: Object,\n        parent_product_tmpl_id: { type: Number, optional: true },\n        price_info: { type: String, optional: true },\n    };\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Return the price, in the format of the given currency.\n     *\n     * @return {String} - The price, in the format of the given currency.\n     */\n    getFormattedPrice() {\n        return formatCurrency(this.props.price, this.env.currency.id);\n    }\n\n    /**\n     * Check whether this product is the main product.\n     *\n     * @return {Boolean} - Whether this product is the main product.\n     */\n    get isMainProduct() {\n        return this.env.mainProductTmplId === this.props.product_tmpl_id;\n    }\n\n    /**\n     * Return this product's image URL.\n     *\n     * @return {String} This product's image URL.\n     */\n    get imageUrl() {\n        const modelPath = this.props.id\n            ? `product.product/${ this.props.id }`\n            : `product.template/${ this.props.product_tmpl_id }`;\n        return `/web/image/${ modelPath }/image_128`;\n    }\n\n    /**\n     * Check whether the provided PTAL should be shown.\n     *\n     * @return {Boolean} Whether the PTAL should be shown.\n     */\n    shouldShowPtal(ptal) {\n        return this.env.canChangeVariant || ptal.create_variant === 'no_variant' ;\n    }\n}\n", "import { Component } from '@odoo/owl';\nimport { BadgeExtraPrice } from '../badge_extra_price/badge_extra_price';\nimport { ProductProduct } from '../models/product_product';\n\nexport class ProductCard extends Component {\n    static template = 'sale.ProductCard';\n    static components = { BadgeExtraPrice };\n    static props = {\n        product: ProductProduct,\n        extraPrice: { type: Number, optional: true },\n        onClick: Function,\n        isSelected: { type: Boolean, optional: true },\n    };\n\n    /**\n     * Check whether the provided PTAL should be shown in this card.\n     *\n     * @param {ProductTemplateAttributeLine} ptal The PTAL to check.\n     * @return {Boolean} Whether to show the PTAL.\n     */\n    shouldShowPtal(ptal) {\n        return ptal.hasSelectedCustomPtav || ptal.create_variant === 'no_variant';\n    }\n}\n", "import { Component, onWillStart, useState, useSubEnv } from \"@odoo/owl\";\nimport { Dialog } from '@web/core/dialog/dialog';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { ProductList } from \"../product_list/product_list\";\n\nexport class ProductConfiguratorDialog extends Component {\n    static components = { Dialog, ProductList};\n    static template = 'sale.ProductConfiguratorDialog';\n    static props = {\n        productTemplateId: Number,\n        ptavIds: { type: Array, element: Number },\n        customPtavs: {\n            type: Array,\n            element: Object,\n            shape: {\n                id: Number,\n                value: String,\n            }\n        },\n        quantity: Number,\n        productUOMId: { type: Number, optional: true },\n        companyId: { type: Number, optional: true },\n        pricelistId: { type: Number, optional: true },\n        currencyId: { type: Number, optional: true },\n        soDate: String,\n        edit: { type: Boolean, optional: true },\n        options: {\n            type: Object,\n            optional: true,\n            shape: {\n                canChangeVariant: { type: Boolean, optional: true },\n                showQuantityAndPrice: { type: Boolean, optional: true },\n            },\n        },\n        save: Function,\n        discard: Function,\n        close: Function, // This is the close from the env of the Dialog Component\n    };\n    static defaultProps = {\n        edit: false,\n    }\n\n    setup() {\n        this.title = _t(\"Configure your product\");\n        this.env.dialogData.dismiss = !this.props.edit && this.props.discard.bind(this);\n        this.state = useState({\n            products: [],\n            optionalProducts: [],\n        });\n        // Nest the currency id in an object so that it stays up to date in the `env`, even if we\n        // modify it in `onWillStart` afterwards.\n        this.currency = { id: this.props.currencyId };\n        this.getValuesUrl = '/sale/product_configurator/get_values';\n        this.createProductUrl = '/sale/product_configurator/create_product';\n        this.updateCombinationUrl = '/sale/product_configurator/update_combination';\n        this.getOptionalProductsUrl = '/sale/product_configurator/get_optional_products';\n\n        useSubEnv({\n            mainProductTmplId: this.props.productTemplateId,\n            currency: this.currency,\n            canChangeVariant: this.props.options?.canChangeVariant ?? true,\n            showQuantityAndPrice: this.props.options?.showQuantityAndPrice ?? true,\n            addProduct: this._addProduct.bind(this),\n            removeProduct: this._removeProduct.bind(this),\n            setQuantity: this._setQuantity.bind(this),\n            updateProductTemplateSelectedPTAV: this._updateProductTemplateSelectedPTAV.bind(this),\n            updatePTAVCustomValue: this._updatePTAVCustomValue.bind(this),\n            isPossibleCombination: this._isPossibleCombination,\n        });\n\n        onWillStart(async () => {\n            const {\n                products,\n                optional_products,\n                currency_id,\n            } = await this._loadData(this.props.edit);\n            this.state.products = products;\n            this.state.optionalProducts = optional_products;\n            for (const customPtav of this.props.customPtavs) {\n                this._updatePTAVCustomValue(\n                    this.env.mainProductTmplId,\n                    customPtav.id,\n                    customPtav.value\n                );\n            }\n            this._checkExclusions(this.state.products[0]);\n            // Use the currency id retrieved from the server if none was provided in the props.\n            this.currency.id ??= currency_id;\n        });\n    }\n\n    //--------------------------------------------------------------------------\n    // Data Exchanges\n    //--------------------------------------------------------------------------\n\n    async _loadData(onlyMainProduct) {\n        return rpc(this.getValuesUrl, {\n            product_template_id: this.props.productTemplateId,\n            quantity: this.props.quantity,\n            currency_id: this.currency.id,\n            so_date: this.props.soDate,\n            product_uom_id: this.props.productUOMId,\n            company_id: this.props.companyId,\n            pricelist_id: this.props.pricelistId,\n            ptav_ids: this.props.ptavIds,\n            only_main_product: onlyMainProduct,\n            ...this._getAdditionalRpcParams(),\n        });\n    }\n\n    async _createProduct(product) {\n        return rpc(this.createProductUrl, {\n            product_template_id: product.product_tmpl_id,\n            ptav_ids: this._getCombination(product),\n        });\n    }\n\n    async _updateCombination(product, quantity) {\n        return rpc(this.updateCombinationUrl, {\n            product_template_id: product.product_tmpl_id,\n            ptav_ids: this._getCombination(product),\n            currency_id: this.currency.id,\n            so_date: this.props.soDate,\n            quantity: quantity,\n            product_uom_id: this.props.productUOMId,\n            company_id: this.props.companyId,\n            pricelist_id: this.props.pricelistId,\n            ...this._getAdditionalRpcParams(),\n        });\n    }\n\n    async _getOptionalProducts(product) {\n        return rpc(this.getOptionalProductsUrl, {\n            product_template_id: product.product_tmpl_id,\n            ptav_ids: this._getCombination(product),\n            parent_ptav_ids: this._getParentsCombination(product),\n            currency_id: this.currency.id,\n            so_date: this.props.soDate,\n            company_id: this.props.companyId,\n            pricelist_id: this.props.pricelistId,\n            ...this._getAdditionalRpcParams(),\n        });\n    }\n\n    /**\n     * Hook to append additional RPC params in overriding modules.\n     *\n     * @return {Object} - The additional RPC params.\n     */\n    _getAdditionalRpcParams() {\n        return {};\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Add the product to the list of products and fetch his optional products.\n     *\n     * @param {Number} productTmplId - The product template id, as a `product.template` id.\n     */\n    async _addProduct(productTmplId) {\n        const index = this.state.optionalProducts.findIndex(\n            p => p.product_tmpl_id === productTmplId\n        );\n        if (index >= 0) {\n            this.state.products.push(...this.state.optionalProducts.splice(index, 1));\n            // Fetch optional product from the server with the parent combination.\n            const product = this._findProduct(productTmplId);\n            // Filter out optional products that are already loaded in the configurator.\n            const newOptionalProducts = (await this._getOptionalProducts(product)).filter(\n                p => !this._findProduct(p.product_tmpl_id)\n            );\n            this.state.optionalProducts.push(...newOptionalProducts);\n        }\n    }\n\n    /**\n     * Remove the product and his optional products from the list of products.\n     *\n     * @param {Number} productTmplId - The product template id, as a `product.template` id.\n     */\n    _removeProduct(productTmplId) {\n        const index = this.state.products.findIndex(p => p.product_tmpl_id === productTmplId);\n        if (index >= 0) {\n            this.state.optionalProducts.push(...this.state.products.splice(index, 1));\n            for (const childProduct of this._getChildProducts(productTmplId)) {\n                this._removeProduct(childProduct.product_tmpl_id);\n                this.state.optionalProducts.splice(\n                    this.state.optionalProducts.findIndex(\n                        p => p.product_tmpl_id === childProduct.product_tmpl_id\n                    ), 1\n                );\n            }\n        }\n    }\n\n    /**\n     * Set the quantity of the product to a given value.\n     *\n     * If the value is less than or equal to zero, the product is removed from the product list\n     * instead, unless it is the main product, in which case the quantity is set to 1.\n     *\n     * @param {Number} productTmplId - The product template id, as a `product.template` id.\n     * @param {Number} quantity - The new quantity of the product.\n     * @return {Boolean} - Whether the quantity was updated.\n     */\n    async _setQuantity(productTmplId, quantity) {\n        if (quantity <= 0) {\n            if (productTmplId === this.env.mainProductTmplId) {\n                quantity = 1;\n            } else {\n                this._removeProduct(productTmplId);\n                return true;\n            }\n        }\n        const product = this._findProduct(productTmplId);\n        if (product.quantity === quantity) {\n            return false;\n        }\n        const { price } = await this._updateCombination(product, quantity);\n        product.quantity = quantity;\n        product.price = parseFloat(price);\n        return true;\n    }\n\n    /**\n     * Change the value of `selected_attribute_value_ids` on the given PTAL in the product.\n     *\n     * @param {Number} productTmplId - The product template id, as a `product.template` id.\n     * @param {Number} ptalId - The PTAL id, as a `product.template.attribute.line` id.\n     * @param {Number} ptavId - The PTAV id, as a `product.template.attribute.value` id.\n     * @param {Boolean} isMulti - Whether multiple `product.template.attribute.value` can be selected.\n     */\n    async _updateProductTemplateSelectedPTAV(productTmplId, ptalId, ptavId, isMulti) {\n        const product = this._findProduct(productTmplId);\n        const ptal = product.attribute_lines.find(line => line.id === ptalId);\n        ptavId = parseInt(ptavId);\n        if (isMulti) {\n            const selectedPtavIds = new Set(ptal.selected_attribute_value_ids);\n            selectedPtavIds.has(ptavId)\n                ? selectedPtavIds.delete(ptavId)\n                : selectedPtavIds.add(ptavId);\n            ptal.selected_attribute_value_ids = Array.from(selectedPtavIds);\n        } else {\n            ptal.selected_attribute_value_ids = [ptavId];\n        }\n        this._checkExclusions(product);\n        if (this._isPossibleCombination(product)) {\n            const updatedValues = await this._updateCombination(product, product.quantity);\n            Object.assign(product, updatedValues);\n            // When a combination should exist but was deleted from the database, it should not be\n            // selectable and considered as an exclusion.\n            if (!product.id && product.attribute_lines.every(ptal => ptal.create_variant === \"always\")) {\n                const combination = this._getCombination(product);\n                product.archived_combinations = product.archived_combinations.concat([combination]);\n                this._checkExclusions(product);\n            }\n        }\n    }\n\n    /**\n     * Set the custom value for a given custom PTAV.\n     *\n     * @param {Number} productTmplId - The product template id, as a `product.template` id.\n     * @param {Number} ptavId - The PTAV id, as a `product.template.attribute.value` id.\n     * @param {String} customValue - The custom value.\n     */\n    _updatePTAVCustomValue(productTmplId, ptavId, customValue) {\n        const product = this._findProduct(productTmplId);\n        product.attribute_lines.find(\n            ptal => ptal.selected_attribute_value_ids.includes(ptavId)\n        ).customValue = customValue;\n    }\n\n    /**\n     * Check the exclusions of a given product and his child.\n     *\n     * @param {Object} product - The product for which to check the exclusions.\n     */\n    _checkExclusions(product) {\n        const combination = this._getCombination(product);\n        const exclusions = product.exclusions;\n        const parentExclusions = product.parent_exclusions;\n        const archivedCombinations = product.archived_combinations;\n        const parentCombination = this._getParentsCombination(product);\n        const childProducts = this._getChildProducts(product.product_tmpl_id)\n        const ptavList = product.attribute_lines.flat().flatMap(ptal => ptal.attribute_values)\n        ptavList.map(ptav => ptav.excluded = false); // Reset all the values\n\n        if (exclusions) {\n            for(const ptavId of combination) {\n                for(const excludedPtavId of exclusions[ptavId]) {\n                    ptavList.find(ptav => ptav.id === excludedPtavId).excluded = true;\n                }\n            }\n        }\n        if (parentCombination) {\n            for(const ptavId of parentCombination) {\n                for(const excludedPtavId of (parentExclusions[ptavId]||[])) {\n                    ptavList.find(ptav => ptav.id === excludedPtavId).excluded = true;\n                }\n            }\n        }\n        if (archivedCombinations) {\n            for(const excludedCombination of archivedCombinations) {\n                const ptavCommon = excludedCombination.filter((ptav) => combination.includes(ptav));\n                if (ptavCommon.length === combination.length) {\n                    for(const excludedPtavId of ptavCommon) {\n                        ptavList.find(ptav => ptav.id === excludedPtavId).excluded = true;\n                    }\n                } else if (ptavCommon.length === (combination.length - 1)) {\n                    // In this case we only need to disable the remaining ptav\n                    const disabledPtavId = excludedCombination.find(\n                        (ptav) => !combination.includes(ptav)\n                    );\n                    const excludedPtav = ptavList.find(ptav => ptav.id === disabledPtavId)\n                    if (excludedPtav) {\n                        excludedPtav.excluded = true;\n                    }\n                }\n            }\n        }\n        for(const optionalProductTmpl of childProducts) {\n            this._checkExclusions(optionalProductTmpl);\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Return the product given his template id.\n     *\n     * @param {Number} productTmplId - The product template id, as a `product.template` id.\n     * @return {Object} - The product.\n     */\n    _findProduct(productTmplId) {\n        // The product might be in either of the two lists `products` or `optional_products`.\n        return  this.state.products.find(p => p.product_tmpl_id === productTmplId) ||\n                this.state.optionalProducts.find(p => p.product_tmpl_id === productTmplId);\n    }\n\n    /**\n     * Return the list of dependents products for a given product.\n     *\n     * @param {Number} productTmplId - The product template id for which to find his children, as a\n     *                                 `product.template` id.\n     * @return {Array} - The list of dependents products.\n     */\n    _getChildProducts(productTmplId) {\n        return [\n            ...this.state.products.filter(p => p.parent_product_tmpl_id === productTmplId),\n            ...this.state.optionalProducts.filter(p => p.parent_product_tmpl_id === productTmplId)\n        ]\n    }\n\n    /**\n     * Return the selected PTAV of the product, as a list of `product.template.attribute.value` id.\n     *\n     * @param {Object} product - The product for which to find the combination.\n     * @return {Array} - The combination of the product.\n     */\n    _getCombination(product) {\n        return product.attribute_lines.flatMap(ptal => ptal.selected_attribute_value_ids);\n    }\n\n    /**\n     * Return the selected PTAVs of the parent product, as a list of\n     * `product.template.attribute.value` ids.\n     *\n     * @param {Object} product - The product for which to find the parent combination.\n     * @return {Array} - The combination of the parent product.\n     */\n    _getParentsCombination(product) {\n        return product.parent_product_tmpl_id\n            ? this._getCombination(this._findProduct(product.parent_product_tmpl_id))\n            : [];\n    }\n\n    /**\n     * Check if a product has a valid combination.\n     *\n     * @param {Object} product - The product for which to check the combination.\n     * @return {Boolean} - Whether the combination is valid or not.\n     */\n    _isPossibleCombination(product) {\n        return product.attribute_lines.every(ptal => {\n            const selectedPtavIds = new Set(ptal.selected_attribute_value_ids);\n            return ptal.attribute_values\n                .filter(ptav => selectedPtavIds.has(ptav.id))\n                .every(ptav => !ptav.excluded);\n        });\n    }\n\n    /**\n     * Check if all the products selected have a valid combination.\n     *\n     * @return {Boolean} - Whether all the products selected have a valid combination or not.\n     */\n    isPossibleConfiguration() {\n        return [...this.state.products].every(\n            p => this._isPossibleCombination(p)\n        );\n    }\n\n    /**\n     * Confirm the current combination(s).\n     *\n     * @return {undefined}\n     */\n    async onConfirm(options) {\n        if (!this.isPossibleConfiguration()) return;\n        // Create the products with dynamic attributes\n        for (const product of this.state.products) {\n            if (\n                !product.id &&\n                product.attribute_lines.some(ptal => ptal.create_variant === \"dynamic\")\n            ) {\n                const productId = await this._createProduct(product);\n                product.id = parseInt(productId);\n            }\n        }\n        await this.props.save(\n            this.state.products.find(\n                p => p.product_tmpl_id === this.env.mainProductTmplId\n            ),\n            this.state.products.filter(\n                p => p.product_tmpl_id !== this.env.mainProductTmplId\n            ),\n            options,\n        );\n        this.props.close();\n    }\n\n    /**\n     * Discard the modal.\n     */\n    onDiscard() {\n        if (!this.props.edit) {\n            this.props.discard(); // clear the line\n        }\n        this.props.close();\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { formatCurrency } from \"@web/core/currency\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Product } from \"../product/product\";\n\nexport class ProductList extends Component {\n    static components = { Product };\n    static template = \"sale.ProductList\";\n    static props = {\n        products: Array,\n        areProductsOptional: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        areProductsOptional: false,\n    };\n\n    setup() {\n        this.optionalProductsTitle = _t(\"Add optional products\");\n    }\n\n    get totalMessage() {\n        return _t(\"Total: %s\", this.getFormattedTotal());\n    }\n\n    /**\n     * Return the total of the product in the list, in the currency of the `sale.order`.\n     *\n     * @return {String} - The sum of all items in the list, in the currency of the `sale.order`.\n     */\n    getFormattedTotal() {\n        return formatCurrency(\n            this.props.products.reduce(\n                (totalPrice, product) => totalPrice + product.price * product.quantity,\n                0\n            ),\n            this.env.currency.id,\n        );\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { formatCurrency } from \"@web/core/currency\";\nimport { BadgeExtraPrice } from \"../badge_extra_price/badge_extra_price\";\n\nexport class ProductTemplateAttributeLine extends Component {\n    static components = { BadgeExtraPrice };\n    static template = \"sale.ProductTemplateAttributeLine\";\n    static props = {\n        productTmplId: Number,\n        id: Number,\n        attribute: {\n            type: Object,\n            shape: {\n                id: Number,\n                name: String,\n                display_type: {\n                    type: String,\n                    validate: type => [\"color\", \"multi\", \"pills\", \"radio\", \"select\"].includes(type),\n                },\n            },\n        },\n        attribute_values: {\n            type: Array,\n            element: {\n                type: Object,\n                shape: {\n                    id: Number,\n                    name: String,\n                    html_color: [Boolean, String], // backend sends 'false' when there is no color\n                    image: [Boolean, String], // backend sends 'false' when there is no image set\n                    is_custom: Boolean,\n                    price_extra: Number,\n                    excluded: { type: Boolean, optional: true },\n                },\n            },\n        },\n        selected_attribute_value_ids: { type: Array, element: Number },\n        create_variant: {\n            type: String,\n            validate: type => [\"always\", \"dynamic\", \"no_variant\"].includes(type),\n        },\n        customValue: {type: [{value: false}, String], optional: true},\n    };\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Update the selected PTAV in the state.\n     *\n     * @param {Event} event\n     */\n    updateSelectedPTAV(event) {\n        this.env.updateProductTemplateSelectedPTAV(\n            this.props.productTmplId, this.props.id, event.target.value, this.props.attribute.display_type == 'multi'\n        );\n    }\n\n    /**\n     * Update in the state the custom value of the selected PTAV.\n     *\n     * @param {Event} event\n     */\n    updateCustomValue(event) {\n        this.env.updatePTAVCustomValue(\n            this.props.productTmplId, this.props.selected_attribute_value_ids[0], event.target.value\n        );\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Return template name to use by checking the display type in the props.\n     *\n     * Each attribute line can have one of this five display types:\n     *      - 'Color'  : Display each attribute as a circle filled with said color.\n     *      - 'Pills'  : Display each attribute as a rectangle-shaped element.\n     *      - 'Radio'  : Display each attribute as a radio element.\n     *      - 'Select' : Display each attribute in a selection tag.\n     *      - 'Multi'  : Display each attribute in a multi-checkbox tag.\n     *\n     * @return {String} - The template name to use.\n     */\n    getPTAVTemplate() {\n        switch(this.props.attribute.display_type) {\n            case 'select':\n                return 'sale.ptav_select';\n            case 'radio':\n                return 'sale.ptav_radio';\n            case 'pills':\n                return 'sale.ptav_pills';\n            case 'color':\n                return 'sale.ptav_color';\n            case 'multi':\n                return 'sale.ptav_multi';\n        }\n    }\n\n    /**\n     * Return the name of the PTAV\n     *\n     * In the selection HTML tag, it is impossible to show the component `BadgeExtraPrice`. Append\n     * the extra price to the name to ensure that the extra price will be shown.\n     * Note: used in `sale.ptav_select`.\n     *\n     * @param {Object} ptav - The attribute, as a `product.template.attribute.value` summary dict.\n     * @return {String} - The name of the PTAV.\n     */\n    getPTAVSelectName(ptav) {\n        if (ptav.price_extra) {\n            const sign = ptav.price_extra > 0 ? '+' : '-';\n            const price = formatCurrency(Math.abs(ptav.price_extra), this.env.currency.id);\n            return ptav.name +\" (\"+ sign + \" \" + price + \")\";\n        } else {\n            return ptav.name;\n        }\n    }\n\n    /**\n     * Check if the selected ptav is custom or not.\n     *\n     * @return {Boolean} - Whether the selected ptav is custom or not.\n     */\n    isSelectedPTAVCustom() {\n        return this.props.attribute_values.find(\n            ptav => this.props.selected_attribute_value_ids.includes(ptav.id)\n        )?.is_custom;\n    }\n\n    get showValuesChoice() {\n        return this.props.attribute_values.length > 1\n            || this.props.attribute.display_type == 'multi'\n    }\n\n    /**\n     * Check if the line has a custom ptav or not.\n     *\n     * @return {Boolean} - Whether the line has a custom ptav or not.\n     */\n    hasPTAVCustom() {\n        return this.props.attribute_values.some(\n            ptav => ptav.is_custom\n        );\n    }\n }\n", "/** @odoo-module */\n\nimport { Component } from '@odoo/owl';\n\nexport class QuantityButtons extends Component {\n    static template = 'sale.QuantityButtons';\n    static props = {\n        quantity: Number,\n        setQuantity: Function,\n        isMinusButtonDisabled: { type: Boolean, optional: true },\n        isPlusButtonDisabled: { type: Boolean, optional: true },\n    };\n\n    /**\n     * Increase the quantity.\n     */\n    increaseQuantity() {\n        this.props.setQuantity(this.props.quantity + 1);\n    }\n\n    /**\n     * Decrease the quantity.\n     */\n    decreaseQuantity() {\n        this.props.setQuantity(this.props.quantity - 1);\n    }\n\n    /**\n     * Set the quantity to a specified value.\n     *\n     * @param {Event} event The quantity input's `on change` event, containing the new quantity.\n     */\n    async setQuantity(event) {\n        const quantity = parseFloat(event.target.value);\n        const didUpdateQuantity = await this.props.setQuantity(isNaN(quantity) ? 0 : quantity);\n        // If the quantity wasn't updated, the component won't rerender, and the input will display\n        // a stale value. As a result, we need to manually rerender the input.\n        if (!didUpdateQuantity) {\n            this.render();\n        }\n    }\n}\n", "/**\n * Checks whether the 2 provided sale order lines are linked.\n *\n * @param linkingSaleOrderLine The line that is linking to the other line.\n * @param linkedSaleOrderLine The line that is linked by the other line.\n * @return {Boolean} Whether the 2 lines are linked.\n */\nexport function areSaleOrderLinesLinked(linkingSaleOrderLine, linkedSaleOrderLine) {\n    const linkingId = linkedSaleOrderLine.isNew\n        ? linkingSaleOrderLine.data.linked_virtual_id\n        : linkingSaleOrderLine.data.linked_line_id[0];\n    const linkedId = linkedSaleOrderLine.isNew\n        ? linkedSaleOrderLine.data.virtual_id\n        : linkedSaleOrderLine.resId;\n    return linkingId && linkingId === linkedId;\n}\n\n/**\n * Gets the linked lines of the provided sale order line.\n *\n * @param saleOrderLine The line whose linked lines to get.\n * @return {Object[]} The list of linked lines.\n */\nexport function getLinkedSaleOrderLines(saleOrderLine) {\n    const saleOrder = saleOrderLine.model.root;\n    // TODO(loti): this leaves out any combo items that are on another page.\n    return saleOrder.data.order_line.records.filter(\n        record => areSaleOrderLinesLinked(record, saleOrderLine)\n    );\n}\n\n/**\n * Serialize a combo item into a format understandable by the server.\n *\n * @param {ProductComboItem} comboItem The combo item to serialize.\n * @return {Object} The serialized combo item.\n */\nexport function serializeComboItem(comboItem) {\n    return {\n        combo_item_id: comboItem.id,\n        product_id: comboItem.product.id,\n        no_variant_attribute_value_ids: comboItem.product.selectedNoVariantPtavIds,\n        product_custom_attribute_values: comboItem.product.selectedCustomPtavs.map(\n            customPtav => ({\n                custom_product_template_attribute_value_id: customPtav.id,\n                custom_value: customPtav.value,\n            })\n        ),\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from '@web/core/l10n/translation';\nimport { patch } from '@web/core/utils/patch';\nimport {\n    ComboConfiguratorDialog\n} from '@sale/js/combo_configurator_dialog/combo_configurator_dialog';\n\npatch(ComboConfiguratorDialog, {\n    props: {\n        ...ComboConfiguratorDialog.props,\n        isFrontend: { type: Boolean, optional: true },\n        // The following fields are needed for tracking.\n        category_name: { type: String, optional: true },\n        currency_name: { type: String, optional: true },\n    },\n});\n\npatch(ComboConfiguratorDialog.prototype, {\n    setup() {\n        super.setup(...arguments);\n\n        if (this.props.isFrontend) {\n            this.getPriceUrl = '/website_sale/combo_configurator/get_price';\n        }\n    },\n\n    get totalMessage() {\n        if (this.props.isFrontend) {\n            return _t(\"Total: %s\", this.formattedTotalPrice);\n        }\n        return super.totalMessage(...arguments);\n    },\n\n    get _comboProductData() {\n        const comboProductData = super._comboProductData;\n        if (this.props.isFrontend) {\n            Object.assign(comboProductData, { 'price': this._totalPrice });\n        }\n        return comboProductData;\n    },\n});\n", "/** @odoo-module **/\n\nimport { formatCurrency } from '@web/core/currency';\nimport { patch } from '@web/core/utils/patch';\nimport { Product } from '@sale/js/product/product';\n\npatch(Product, {\n    props: {\n        ...Product.props,\n        strikethrough_price: { type: Number, optional: true },\n        can_be_sold: { type: Boolean, optional: true },\n        // The following fields are needed for tracking.\n        category_name: { type: String, optional: true },\n        currency_name: { type: String, optional: true },\n    },\n});\n\npatch(Product.prototype, {\n    /**\n     * Return the strikethrough price, formatted using the environment's currency.\n     *\n     * @return {String} - The formatted strikethrough price.\n     */\n    get formattedStrikethroughPrice() {\n        return formatCurrency(this.props.strikethrough_price, this.env.currency.id);\n    },\n});\n", "import { _t } from '@web/core/l10n/translation';\nimport { patch } from '@web/core/utils/patch';\nimport { useSubEnv } from '@odoo/owl';\nimport {\n    ProductConfiguratorDialog\n} from '@sale/js/product_configurator_dialog/product_configurator_dialog';\n\npatch(ProductConfiguratorDialog, {\n    props: {\n        ...ProductConfiguratorDialog.props,\n        isFrontend: { type: Boolean, optional: true },\n        options: {\n            ...ProductConfiguratorDialog.props.options,\n            shape: {\n                ...ProductConfiguratorDialog.props.options.shape,\n                isMainProductConfigurable: { type: Boolean, optional: true },\n            },\n        },\n    },\n});\n\npatch(ProductConfiguratorDialog.prototype, {\n    setup() {\n        super.setup(...arguments);\n\n        if (this.props.isFrontend) {\n            this.getValuesUrl = '/website_sale/product_configurator/get_values';\n            this.createProductUrl = '/website_sale/product_configurator/create_product';\n            this.updateCombinationUrl = '/website_sale/product_configurator/update_combination';\n            this.getOptionalProductsUrl = '/website_sale/product_configurator/get_optional_products';\n            // To be translated, the title must be repeated here. Indeed, only\n            // translations of \"frontend modules\" are fetched in the context of\n            // website. The original definition of the title is in \"sale\", which\n            // is not a frontend module.\n            this.title = _t(\"Configure your product\");\n        }\n\n        useSubEnv({\n            isFrontend: this.props.isFrontend,\n            isMainProductConfigurable: this.props.options?.isMainProductConfigurable ?? true,\n        });\n    },\n\n    /**\n     * Check whether all selected products can be sold.\n     *\n     * @return {Boolean} - Whether all selected products can be sold.\n     */\n    canBeSold() {\n        return this.state.products.every(p => p.can_be_sold);\n    },\n});\n", "/** @odoo-module **/\n\nimport { patch } from '@web/core/utils/patch';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ProductList } from '@sale/js/product_list/product_list';\n\npatch(ProductList.prototype, {\n    setup() {\n        super.setup(...arguments);\n\n        if (this.env.isFrontend) {\n            this.optionalProductsTitle = _t(\"Available options\");\n        }\n    },\n\n    get totalMessage() {\n        if (this.env.isFrontend) {\n            return _t(\"Total: %s\", this.getFormattedTotal());\n        }\n        return super.totalMessage(...arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport { patch } from '@web/core/utils/patch';\nimport {\n    ProductTemplateAttributeLine\n} from '@sale/js/product_template_attribute_line/product_template_attribute_line';\n\npatch(ProductTemplateAttributeLine.prototype, {\n    /**\n     * Return the display name of this PTAL.\n     *\n     * @return {String} - The display name of this PTAL.\n     */\n    getPtalDisplayName() {\n        const selectedPtavIds = new Set(this.props.selected_attribute_value_ids);\n        const selectedPtavNames = this.props.attribute_values\n            .filter(ptav => selectedPtavIds.has(ptav.id))\n            .map(ptav => ptav.name)\n            .join(', ');\n        let ptalDisplayName = `${this.props.attribute.name}: ${selectedPtavNames}`;\n        if (this.isSelectedPTAVCustom()) {\n            ptalDisplayName += `: ${this.props.customValue}`;\n        }\n        return ptalDisplayName;\n    },\n});\n", "/** @odoo-module **/\n\nimport {\n    LocationSchedule\n} from '@delivery/js/location_selector/location_schedule/location_schedule';\nimport { Component } from '@odoo/owl';\n\nexport class Location extends Component {\n    static components = { LocationSchedule };\n    static template = 'delivery.locationSelector.location';\n    static props = {\n        id: String,\n        number: Number,\n        name: String,\n        street: String,\n        city: String,\n        zipCode: String,\n        openingHours: {\n            type: Object,\n            values: {\n                type: Array,\n                element: String,\n                optional: true,\n            },\n        },\n        additionalData: { type: Object, optional: true },\n        isSelected: Boolean,\n        setSelectedLocation: Function,\n    };\n\n    /**\n     * Get the city and the zip code.\n     *\n     * @return {Object} The city and the zip code.\n     */\n    getCityAndZipCode() {\n        return `${this.props.zipCode} ${this.props.city}`;\n    }\n}\n", "/** @odoo-module **/\n\nimport { Location } from '@delivery/js/location_selector/location/location';\nimport { Component, onMounted, useEffect } from '@odoo/owl';\n\nexport class LocationList extends Component {\n    static components = { Location };\n    static template = 'delivery.locationSelector.locationList';\n    static props = {\n        locations: {\n            type: Array,\n            element: {\n                type: Object,\n                values: {\n                    id: String,\n                    name: String,\n                    openingHours: {\n                        type: Object,\n                        values: {\n                            type: Array,\n                            element: String,\n                            optional: true,\n                        },\n                    },\n                    street: String,\n                    city: String,\n                    zip_code: String,\n                    state: { type: String, optional: true},\n                    country_code: String,\n                    additional_data: { type: Object, optional: true},\n                    latitude: String,\n                    longitude: String,\n                }\n            },\n        },\n        selectedLocationId: [String, {value: false}],\n        setSelectedLocation: Function,\n        validateSelection: Function,\n    };\n\n    setup() {\n        onMounted(() => {\n            document.getElementById(`location-${this.props.selectedLocationId}`).focus();\n        });\n\n        // Focus on the location on the list when clicking on the map marker.\n        useEffect(\n            (locations, selectedLocationId) => {\n                const selectedLocation = locations.find(\n                    l => String(l.id) === selectedLocationId\n                );\n                if (selectedLocation) {\n                    document.getElementById(`location-${selectedLocation.id}`).focus();\n                }\n            },\n            () => [this.props.locations, this.props.selectedLocationId]\n        );\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component } from '@odoo/owl';\n\nexport class LocationSchedule extends Component {\n    static template = 'delivery.locationSelector.schedule';\n    static props = {\n        openingHours: {\n            type: Object,\n            values: {\n                type: Array,\n                element: String,\n                optional: true,\n            },\n        },\n        wrapClass: { type: String, optional: true },\n    };\n\n    /**\n     * Return the localized day's name given his index in the week.\n     *\n     * @param {Number} weekday - The number of the day of the week. 0 for Monday, 6 for Sunday.\n     * @return {Object} the localized name of the day (long version).\n     */\n    getWeekDay(weekday) {\n        return luxon.Info.weekdays()[weekday]\n    }\n}\n", "/** @odoo-module **/\n\nimport { LocationList } from '@delivery/js/location_selector/location_list/location_list';\nimport { MapContainer } from '@delivery/js/location_selector/map_container/map_container';\nimport { Component, onMounted, onWillUnmount, useEffect, useState } from '@odoo/owl';\nimport { browser } from '@web/core/browser/browser';\nimport { Dialog } from '@web/core/dialog/dialog';\nimport { _t } from '@web/core/l10n/translation';\nimport { rpc } from '@web/core/network/rpc';\nimport { useDebounced } from '@web/core/utils/timing';\n\nexport class LocationSelectorDialog extends Component {\n    static components = { Dialog, LocationList, MapContainer };\n    static template = 'delivery.locationSelector.dialog';\n    static props = {\n        orderId: Number,\n        zipCode: String,\n        selectedLocationId: { type: String, optional: true},\n        save: Function,\n        close: Function, // This is the close from the env of the Dialog Component\n    };\n    static defaultProps = {\n        selectedLocationId: false,\n    };\n\n    setup() {\n        this.title = _t(\"Choose a pick-up point\");\n        this.state = useState({\n            locations: [],\n            error: false,\n            viewMode: 'list',\n            zipCode: this.props.zipCode,\n            // Some APIs like FedEx use strings to identify locations.\n            selectedLocationId: String(this.props.selectedLocationId),\n            isSmall: this.env.isSmall,\n        });\n\n        this.getLocationUrl = '/delivery/get_pickup_locations';\n\n        this.debouncedOnResize = useDebounced(this.updateSize, 300);\n        this.debouncedSearchButton = useDebounced((zipCode) => {\n            this.state.locations = [];\n            this._updateLocations(zipCode);\n        }, 300);\n\n        onMounted(() => {\n            browser.addEventListener('resize', this.debouncedOnResize);\n            this.updateSize();\n        });\n        onWillUnmount(() => browser.removeEventListener('resize', this.debouncedOnResize));\n\n        // Fetch new locations when the zip code is updated.\n        useEffect(\n            (zipCode) => {\n                this._updateLocations(zipCode)\n                return () => {\n                    this.state.locations = []\n                };\n            },\n            () => [this.state.zipCode]\n        );\n    }\n\n    //--------------------------------------------------------------------------\n    // Data Exchanges\n    //--------------------------------------------------------------------------\n\n    /**\n     * Fetch the closest pickup locations based on the zip code.\n     *\n     * @private\n     * @param {String} zip - The zip code used to look for close locations.\n     * @return {Object} The result values.\n     */\n    async _getLocations(zip) {\n        return rpc(this.getLocationUrl, {order_id: this.props.orderId, zip_code: zip});\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Get the locations based on the zip code.\n     *\n     * Select the first location available if no location is currently selected or if the currently\n     * selected location is not on the list anymore.\n     *\n     * @private\n     * @param {String} zip - The zip code used to look for close locations.\n     * @return {void}\n     */\n    async _updateLocations(zip) {\n        this.state.error = false;\n        const { pickup_locations, error } = await this._getLocations(zip);\n        if (error) {\n            this.state.error = error;\n            console.error(error);\n        } else {\n            this.state.locations = pickup_locations;\n            if (!this.state.locations.find(l => String(l.id) === this.state.selectedLocationId)) {\n                this.state.selectedLocationId = this.state.locations[0]\n                                                ? String(this.state.locations[0].id)\n                                                : false;\n            }\n        }\n    }\n\n    /**\n     * Find the selected location based on its id.\n     *\n     * @return {Object} The selected location.\n     */\n    get selectedLocation() {\n        return this.state.locations.find(l => String(l.id) === this.state.selectedLocationId);\n    }\n\n    /**\n     * Set the selectedLocationId in the state.\n     *\n     * @param {String} locationId\n     * @return {void}\n     */\n    setSelectedLocation(locationId) {\n        this.state.selectedLocationId = String(locationId);\n    }\n\n    /**\n     * Confirm the current selected location.\n     *\n     * @return {void}\n     */\n    async validateSelection() {\n        if (!this.state.selectedLocationId) return;\n        const selectedLocation = this.state.locations.find(\n            l => String(l.id) === this.state.selectedLocationId\n        );\n        await this.props.save(selectedLocation);\n        this.props.close();\n    }\n\n    //--------------------------------------------------------------------------\n    // User Interface\n    //--------------------------------------------------------------------------\n\n    /**\n     * Determines the component to show in mobile view based on the current state.\n     *\n     * Returns the MapContainer component if `viewMode` is strictly equal to `map`, else return the\n     * List component.\n     *\n     * @return {Component} The component to show in mobile view.\n     */\n    get mobileComponent() {\n        if (this.state.viewMode === 'map') return MapContainer;\n        return LocationList;\n    }\n\n    /**\n     *\n     * @return {void}\n     */\n    updateSize() {\n        this.state.isSmall = this.env.isSmall;\n    }\n}\n", "/** @odoo-module **/\n/*global L*/\n\nimport { Component, useEffect, useRef } from '@odoo/owl';\nimport { renderToString } from '@web/core/utils/render';\n\nexport class Map extends Component {\n    static template = 'delivery.locationSelector.map';\n    static props = {\n        locations: {\n            type: Array,\n            element: {\n                type: Object,\n                values: {\n                    id: String,\n                    name: String,\n                    openingHours: {\n                        type: Object,\n                        values: {\n                            type: Array,\n                            element: String,\n                            optional: true,\n                        },\n                    },\n                    street: String,\n                    city: String,\n                    zip_code: String,\n                    state: { type: String, optional: true},\n                    country_code: String,\n                    additional_data: { type: Object, optional: true},\n                    latitude: String,\n                    longitude: String,\n                }\n            },\n        },\n        selectedLocationId: [String, {value: false}],\n        setSelectedLocation: Function,\n    };\n\n    setup() {\n        this.leafletMap = null;\n        this.markers = [];\n        this.mapRef = useRef('map');\n\n        // Create the map.\n        useEffect(\n            () => {\n                this.leafletMap = L.map(this.mapRef.el, {\n                    zoom: 13,\n                });\n                this.leafletMap.attributionControl.setPrefix(\n                    '<a href=\"https://leafletjs.com\" title=\"A JavaScript library for interactive maps\">Leaflet</a>'\n                );\n                L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {\n                    maxZoom: 19,\n                    attribution: \"&copy; <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a>\"\n                }).addTo(this.leafletMap);\n                return () => {\n                    this.leafletMap.remove();\n                }\n            },\n            () => []\n        );\n\n        // Update the size of the map.\n        useEffect(\n            (locations) => {\n                this.leafletMap.invalidateSize();\n            },\n            () => [this.props.locations]\n        );\n\n        // Update the markers and center the map on the selected location.\n        useEffect(\n            (locations, selectedLocationId) => {\n                this.addMarkers(locations);\n                const selectedLocation = locations.find(\n                    l => String(l.id) === selectedLocationId\n                );\n                if (selectedLocation) {\n                    // Center the Map.\n                    this.leafletMap.panTo(\n                        [selectedLocation.latitude, selectedLocation.longitude],\n                        { animate: true }\n                    );\n                }\n                return () => {\n                    this.removeMarkers();\n                };\n            },\n            () => [this.props.locations, this.props.selectedLocationId]\n        );\n    }\n\n    /**\n     * Add the markers of the closest locations on the map.\n     * Binds events to the created markers.\n     *\n     * @param {Array} locations - The list of locations to display on the map.\n     * @return {void}\n     */\n    addMarkers(locations) {\n        for (const loc of locations) {\n            const isSelected = String(loc.id) === this.props.selectedLocationId\n            // Icon creation\n            const iconInfo = {\n                className: isSelected ? 'o_location_selector_marker_icon_selected'\n                                      : 'o_location_selector_marker_icon',\n                html: renderToString(\n                    'delivery.locationSelector.map.marker',\n                    { number: locations.indexOf(loc) + 1 },\n                ),\n            };\n\n            const marker = L.marker(\n                [ loc.latitude, loc.longitude ],\n                {\n                    icon: L.divIcon(iconInfo),\n                    title: locations.indexOf(loc) + 1,\n                },\n            );\n\n            // By default, the marker's zIndex is based on its latitude. This ensures the selected\n            // marker is always displayed on top of all others.\n            if (isSelected) marker.setZIndexOffset(100);\n\n            marker.addTo(this.leafletMap);\n            marker.addEventListener('click', () => {\n                this.props.setSelectedLocation(loc.id);\n            });\n\n            this.markers.push(marker);\n        }\n    }\n\n    /**\n     * Remove the markers from the map and empty the markers array.\n     *\n     * @return {void}\n     */\n    removeMarkers() {\n        for (const marker of this.markers) {\n            marker.removeEventListener();\n            this.leafletMap.removeLayer(marker);\n        }\n        this.markers = [];\n    }\n\n    /**\n     * Find the selected location based on its id.\n     *\n     * @return {Object} The selected location.\n     */\n    get selectedLocation() {\n        return this.props.locations.find(l => String(l.id) === this.props.selectedLocationId)\n    }\n}\n", "/** @odoo-module **/\n\nimport {\n    LocationSchedule\n} from '@delivery/js/location_selector/location_schedule/location_schedule';\nimport { Map } from '@delivery/js/location_selector/map/map';\nimport { Component, onWillStart, useState } from '@odoo/owl';\nimport { AssetsLoadingError, loadCSS, loadJS } from '@web/core/assets';\n\nexport class MapContainer extends Component {\n    static components = { LocationSchedule, Map };\n    static template = 'delivery.locationSelector.mapContainer';\n    static props = {\n        locations: {\n            type: Array,\n            element: {\n                type: Object,\n                values: {\n                    id: String,\n                    name: String,\n                    openingHours: {\n                        type: Object,\n                        values: {\n                            type: Array,\n                            element: String,\n                            optional: true,\n                        },\n                    },\n                    street: String,\n                    city: String,\n                    zip_code: String,\n                    state: { type: String, optional: true},\n                    country_code: String,\n                    additional_data: { type: Object, optional: true},\n                    latitude: String,\n                    longitude: String,\n                }\n            },\n        },\n        selectedLocationId: [String, {value: false}],\n        setSelectedLocation: Function,\n        validateSelection: Function,\n    };\n\n    setup() {\n        this.state = useState({\n            shouldLoadMap: false,\n        });\n\n        onWillStart(async () => {\n            /**\n             * We load the script for the map before rendering the owl component to avoid a\n             * UserError if the script can't be loaded (e.g. if the customer loses the connection\n             * between the rendering of the page and when he opens the location selector, or if the\n             * CDN\u2019s doesn't host the library anymore).\n             */\n            try {\n                await Promise.all([\n                    loadJS('https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'),\n                    loadCSS('https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'),\n                ])\n                this.state.shouldLoadMap = true;\n            } catch (error) {\n                if (!(error instanceof AssetsLoadingError)) {\n                    throw error;\n                }\n            }\n        });\n    }\n\n    /**\n     * Get the city and the zip code.\n     *\n     * @param {Number} selectedLocation - The location form which the city and the zip code\n     *                                    should be taken.\n     * @return {Object} The city and the zip code.\n     */\n    getCityAndZipCode(selectedLocation) {\n        return `${selectedLocation.zip_code} ${selectedLocation.city}`;\n    }\n\n    /**\n     * Find the selected location based on its id.\n     *\n     * @return {Object} The selected location.\n     */\n    get selectedLocation() {\n        return this.props.locations.find(l => String(l.id) === this.props.selectedLocationId);\n    }\n}\n", "/** @odoo-module **/\n\nimport {\n    LocationSelectorDialog\n} from '@delivery/js/location_selector/location_selector_dialog/location_selector_dialog';\nimport { patch } from '@web/core/utils/patch';\n\npatch(LocationSelectorDialog, {\n    props: {\n        ...LocationSelectorDialog.props,\n        orderId: { type: Number, optional: true },\n        isFrontend: { type: Boolean, optional: true },\n    },\n});\n\npatch(LocationSelectorDialog.prototype, {\n    setup() {\n        super.setup(...arguments);\n\n        if (this.props.isFrontend) {\n            this.getLocationUrl = '/website_sale/get_pickup_locations';\n        }\n    },\n});\n", "/** @odoo-module **/\n\n    import { registry } from \"@web/core/registry\";\n    import { stepUtils } from \"@web_tour/tour_service/tour_utils\";\n    import { _t } from \"@web/core/l10n/translation\";\n\n    import { markup } from \"@odoo/owl\";\n\n    const { DateTime } = luxon;\n\n    registry.category(\"web_tour.tours\").add('mass_mailing_tour', {\n        url: '/odoo',\n        steps: () => [stepUtils.showAppsMenuItem(), {\n        isActive: [\"enterprise\"],\n        trigger: '.o_app[data-menu-xmlid=\"mass_mailing.mass_mailing_menu_root\"]',\n        content: _t(\"Let's try the Email Marketing app.\"),\n        tooltipPosition: 'bottom',\n        run: \"click\",\n    }, {\n        isActive: [\"community\"],\n        trigger: '.o_app[data-menu-xmlid=\"mass_mailing.mass_mailing_menu_root\"]',\n        content: _t(\"Let's try the Email Marketing app.\"),\n        run: \"click\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: \".o_mass_mailing_mailing_tree\",\n    },\n    {\n        trigger: '.o_list_button_add',\n        content: markup(_t(\"Start by creating your first <b>Mailing</b>.\")),\n        tooltipPosition: 'bottom',\n        run: \"click\",\n    }, {\n        trigger: 'input[name=\"subject\"]',\n        content: markup(_t('Pick the <b>email subject</b>.')),\n        tooltipPosition: 'bottom',\n        run: `edit ${DateTime.now().toFormat(\"LLLL\")} Newsletter`,\n    }, {\n        isActive: [\"auto\"],\n        trigger: 'div[name=\"contact_list_ids\"] > .o_input_dropdown > input[type=\"text\"]',\n        run: 'click',\n    }, {\n        isActive: [\"auto\"],\n        trigger: 'li.ui-menu-item',\n        run: 'click',\n    }, {\n        isActive: [\"enterprise\"],\n        trigger: 'div[name=\"body_arch\"] :iframe #newsletter',\n        content: markup(_t('Choose this <b>theme</b>.')),\n        tooltipPosition: 'left',\n        run: 'click',\n    }, {\n        isActive: [\"community\"],\n        trigger: 'div[name=\"body_arch\"] :iframe #default',\n        content: markup(_t('Choose this <b>theme</b>.')),\n        tooltipPosition: 'right',\n        run: 'click',\n    }, {\n        isActive: [\"enterprise\"],\n        trigger: 'div[name=\"body_arch\"] :iframe div.s_text_block',\n        content: _t('Click on this paragraph to edit it.'),\n        tooltipPosition: 'top',\n        run: 'click',\n    }, {\n        isActive: [\"community\"],\n        trigger: 'div[name=\"body_arch\"] :iframe div.o_mail_block_title_text',\n        content: _t('Click on this paragraph to edit it.'),\n        tooltipPosition: 'top',\n        run: 'click',\n    }, {\n        trigger: 'button[name=\"action_set_favorite\"]',\n        content: _t('Click on this button to add this mailing to your templates.'),\n        tooltipPosition: 'bottom',\n        run: 'click',\n    }, {\n        trigger: 'button[name=\"action_test\"]',\n        content: _t(\"Test this mailing by sending a copy to yourself.\"),\n        tooltipPosition: 'bottom',\n        run: \"click\",\n    }, {\n        trigger: 'button[name=\"send_mail_test\"]',\n        content: _t(\"Check the email address and click send.\"),\n        tooltipPosition: 'bottom',\n        run: \"click\",\n    }, {\n        trigger: 'button[name=\"action_launch\"]',\n        content: _t(\"Ready for take-off!\"),\n        tooltipPosition: 'bottom',\n        run: \"click\",\n    }, {\n        trigger: '.btn-primary:contains(\"Ok\")',\n        content: _t(\"Don't worry, the mailing contact we created is an internal user.\"),\n        tooltipPosition: 'bottom',\n        run: \"click\",\n    }, {\n        trigger: '.o_back_button',\n        content: markup(_t(\"By using the <b>Breadcrumb</b>, you can navigate back to the overview.\")),\n        tooltipPosition: 'bottom',\n        run: 'click',\n    }]\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { rpc } from \"@web/core/network/rpc\";\n\npublicWidget.registry.UnsplashBeacon = publicWidget.Widget.extend({\n    // /!\\ To adapt the day the beacon makes sense for backend customizations\n    selector: '#wrapwrap',\n\n    /**\n     * @override\n     */\n    start: function () {\n        var unsplashImages = Array.from(this.$('img[src*=\"/unsplash/\"]')).map((img) => {\n            // get image id from URL (`http://www.domain.com:1234/unsplash/xYdf5feoI/lion.jpg` -> `xYdf5feoI`)\n            return img.src.split('/unsplash/')[1].split('/')[0];\n        });\n        if (unsplashImages.length) {\n            rpc('/web_unsplash/get_app_id').then(function (appID) {\n                if (!appID) {\n                    return;\n                }\n                $.get('https://views.unsplash.com/v', {\n                    'photo_id': unsplashImages.join(','),\n                    'app_id': appID,\n                });\n            });\n        }\n        return this._super.apply(this, arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport { loadEmoji } from \"@web/core/emoji_picker/emoji_picker\";\n\n// List of icons that should be avoided when adding a random icon\nconst iconsBlocklist = [\"\ud83d\udca9\", \"\ud83d\udc80\", \"\u2620\ufe0f\", \"\ud83e\udd2e\", \"\ud83d\udd95\", \"\ud83e\udd22\", \"\ud83d\ude12\"];\n\n/**\n * Get a random icon (that is not in the icons blocklist)\n * @returns {String} emoji\n */\nexport async function getRandomIcon() {\n    const { emojis } = await loadEmoji();\n    const randomEmojis = emojis.filter((emoji) => !iconsBlocklist.includes(emoji.codepoints));\n    return randomEmojis[Math.floor(Math.random() * randomEmojis.length)].codepoints;\n}\n\n/**\n * Set an intersection observer on the given element. This function will ensure\n * that the given callback function will be called at most once when the given\n * element becomes visible on screen. This function can be used to load\n * components lazily (see: 'EmbeddedViewComponent').\n * @param {HTMLElement} element\n * @param {Function} callback\n * @returns {IntersectionObserver}\n */\nexport function setIntersectionObserver (element, callback) {\n    const options = {\n        root: null,\n        rootMargin: '0px'\n    };\n    const observer = new window.IntersectionObserver(entries => {\n        const entry = entries[0];\n        if (entry.isIntersecting) {\n            observer.unobserve(entry.target);\n            callback();\n        }\n    }, options);\n    observer.observe(element);\n    return observer;\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { renderToString } from \"@web/core/utils/render\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { normalizePosition, pinchService, isVisible } from \"./utils\";\n\nexport class PDFIframe {\n    /**\n     * Renders custom elements inside the PDF.js iframe\n     * @param {HTMLIFrameElement} iframe\n     * @param {Document} root\n     * @param {Object} env\n     * @param {Object} owlServices\n     * @param {Object} props\n     */\n    constructor(root, env, owlServices, props) {\n        this.root = root;\n        this.env = env;\n        Object.assign(this, owlServices);\n        this.props = props;\n        this.cleanupFns = [];\n\n        this.readonly = props.readonly;\n        this.signItemTypesById = this.props.signItemTypes.reduce((obj, type) => {\n            obj[type.id] = type;\n            return obj;\n        }, {});\n        this.selectionOptionsById = this.props.signItemOptions.reduce((obj, option) => {\n            obj[option.id] = option;\n            return obj;\n        }, {});\n        this.radioSets = this.props.radioSets;\n        this.waitForPagesToLoad();\n    }\n\n    waitForPagesToLoad() {\n        const errorElement = this.root.querySelector(\"#errorMessage\");\n        if (isVisible(errorElement)) {\n            return this.dialog.add(AlertDialog, {\n                body: _t(\"Need a valid PDF to add signature fields!\"),\n            });\n        }\n        this.pageCount = this.root.querySelectorAll(\".page\").length;\n        if (this.pageCount > 0) {\n            this.start();\n        } else {\n            setTimeout(() => this.waitForPagesToLoad(), 50);\n        }\n    }\n\n    start() {\n        this.signItems = this.getSignItems();\n        this.loadCustomCSS().then(() => {\n            this.pageCount = this.root.querySelectorAll(\".page\").length;\n            this.clearNativePDFViewerButtons();\n            this.startPinchService();\n            this.preRender();\n            this.renderSidebar();\n            this.addCanvasLayer();\n            this.renderSignItems();\n            this.postRender();\n        });\n    }\n\n    unmount() {\n        this.cleanupFns.forEach((fn) => typeof fn === \"function\" && fn());\n    }\n\n    async loadCustomCSS() {\n        const assets = await rpc(\"/sign/render_assets_pdf_iframe\", {\n            args: [{ debug: this.env.debug }],\n        });\n        this.root.querySelector(\"head\").insertAdjacentHTML(\"beforeend\", assets);\n    }\n\n    clearNativePDFViewerButtons() {\n        const selectors = [\n            \"#pageRotateCw\",\n            \"#pageRotateCcw\",\n            \"#openFile\",\n            \"#presentationMode\",\n            \"#viewBookmark\",\n            \"#print\",\n            \"#download\",\n            \"#secondaryOpenFile\",\n            \"#secondaryPresentationMode\",\n            \"#secondaryViewBookmark\",\n            \"#secondaryPrint\",\n            \"#secondaryDownload\",\n        ];\n        const elements = this.root.querySelectorAll(selectors.join(\", \"));\n        elements.forEach((element) => {\n            element.style.display = \"none\";\n        });\n        this.root.querySelector(\"#lastPage\").nextElementSibling.style.display = \"none\";\n        // prevent password from being autocompleted in search input\n        this.root.querySelector(\"#findInput\").value = \"\";\n        this.root.querySelector(\"#findInput\").setAttribute(\"autocomplete\", \"off\");\n        const passwordInputs = this.root.querySelectorAll(\"[type=password]\");\n        Array.from(passwordInputs).forEach((input) =>\n            input.setAttribute(\"autocomplete\", \"new-password\")\n        );\n    }\n\n    /**\n     * Adds canvas layer used to draw connecting lines between radio items.\n     */\n    addCanvasLayer() {\n        const viewer = this.root.querySelector(\"#viewer\");\n        const layer = document.createElement(\"canvas\");\n        layer.id = \"canvas_layer_0\";\n        layer.style.position = 'absolute';\n        layer.style.top = '0';\n        layer.style.left = '0';\n        layer.style.zIndex = 1;\n        layer.style.width = viewer.offsetWidth + \"px\";\n        layer.style.height = viewer.offsetHeight + \"px\";\n        layer.width = viewer.offsetWidth;\n        layer.height = viewer.offsetHeight;\n        viewer.appendChild(layer);\n    }\n\n    /**\n     * Used when signing a sign request\n     */\n    renderSidebar() {}\n\n    renderSignItems() {\n        for (const page in this.signItems) {\n            const pageContainer = this.getPageContainer(page);\n            for (const id in this.signItems[page]) {\n                const signItem = this.signItems[page][id];\n                signItem.el = this.renderSignItem(signItem.data, pageContainer);\n            }\n        }\n        this.updateFontSize();\n        this.renderAllConnectingLines();\n    }\n\n    /**\n     * Renders connecting lines between radio items.\n     */\n\n    renderAllConnectingLines(){}\n\n    /**\n     * register sign item events. in template edition, should be overwritten to add drag/drop events\n     */\n    enableCustom(signItem) {}\n\n    startPinchService() {\n        const pinchTarget = this.root.querySelector(\"#viewerContainer #viewer\");\n        const pinchServiceCleanup = pinchService(pinchTarget, {\n            decreaseDistanceHandler: () => this.root.querySelector(\"button#zoomIn\").click(),\n            increaseDistanceHandler: () => this.root.querySelector(\"button#zoomOut\").click(),\n        });\n        this.cleanupFns.push(pinchServiceCleanup);\n    }\n\n    /**\n     * Renders a sign item using its data and attaches it to a target html element\n     * @param { Object } signItemData\n     * @property\n     */\n    renderSignItem(signItemData, target) {\n        const signItemElement = renderToString(\"sign.signItem\", this.getContext(signItemData));\n        target.insertAdjacentHTML(\"beforeend\", signItemElement);\n        const signItem = target.lastChild;\n        signItem.classList.add(\"d-none\");\n        this.enableCustom({ el: signItem, data: signItemData });\n        return signItem;\n    }\n\n    /**\n     * Extends the rendering context of the sign item based on its data\n     * @param {SignItem.data} signItem\n     * @returns {Object}\n     */\n    getContext(signItem) {\n        const normalizedPosX =\n            Math.round(normalizePosition(signItem.posX, signItem.width) * 1000) / 1000;\n        const normalizedPosY =\n            Math.round(normalizePosition(signItem.posY, signItem.height) * 1000) / 1000;\n        const responsible = parseInt(signItem.responsible ?? (signItem.responsible_id?.[0] || 0));\n        const type = this.signItemTypesById[signItem.type_id].item_type;\n        if (type === \"selection\") {\n            const options = signItem.option_ids.map((id) => this.selectionOptionsById[id]);\n            signItem.options = options;\n        }\n        // handles prefilled values with 0\n        if (signItem.value === 0) {\n            signItem.value = \"0\";\n        }\n        const readonly =\n            this.readonly ||\n            (responsible > 0 && responsible !== this.currentRole) ||\n            !!signItem.value;\n        const isCurrentRole = this.currentRole === parseInt(responsible);\n        return Object.assign(signItem, {\n            readonly: signItem.readonly ?? readonly,\n            editMode: signItem.editMode ?? false,\n            required: Boolean(signItem.required),\n            responsible,\n            type,\n            placeholder: signItem.placeholder || signItem.name || \"\",\n            classes: `${signItem.required && isCurrentRole ? \"o_sign_sign_item_required\" : \"\"} ${\n                readonly && isCurrentRole ? \"o_readonly_mode\" : \"\"\n            } ${this.readonly ? \"o_sign_sign_item_pdfview\" : \"\"}`,\n            style: `top: ${normalizedPosY * 100}%; left: ${normalizedPosX * 100}%;\n                    width: ${signItem.width * 100}%; height: ${signItem.height * 100}%;\n                    text-align: ${signItem.alignment}`,\n        });\n    }\n\n    /**\n     * PDF.js removes custom elements every once in a while.\n     * So we need to constantly re-render them :(\n     * We keep the elements stored in memory, so we don't need to call the qweb engine everytime a element is detached\n     */\n    refreshSignItems() {\n        for (const page in this.signItems) {\n            const pageContainer = this.getPageContainer(page);\n            for (const id in this.signItems[page]) {\n                const signItem = this.signItems[page][id].el;\n                signItem.classList.remove(\"d-none\");\n                if (!signItem.parentElement || !signItem.parentElement.classList.contains(\"page\")) {\n                    pageContainer.append(signItem);\n                }\n            }\n        }\n        this.updateFontSize();\n        this.renderAllConnectingLines();\n    }\n\n    /**\n     * Hook executed before rendering the sign items and the sidebar\n     */\n    preRender() {\n        const viewerContainer = this.root.querySelector(\"#viewerContainer\");\n        viewerContainer.style.visibility = \"visible\";\n        this.setInitialZoom();\n    }\n\n    get normalSize() {\n        return this.root.querySelector(\".page\").clientHeight * 0.015;\n    }\n\n    /**\n     * Updates the font size of all sign items in case there was a zoom/resize of element\n     */\n    updateFontSize() {\n        for (const page in this.signItems) {\n            for (const id in this.signItems[page]) {\n                const signItem = this.signItems[page][id];\n                this.updateSignItemFontSize(signItem);\n            }\n        }\n    }\n\n    /**\n     * Updates the font size of a determined sign item\n     * @param {SignItem}\n     */\n    updateSignItemFontSize({ el, data }) {\n        const largerTypes = [\"signature\", \"initial\", \"textarea\", \"selection\"];\n        const size = largerTypes.includes(data.type)\n            ? this.normalSize\n            : parseFloat(el.clientHeight);\n        el.style.fontSize = `${size * 0.8}px`;\n    }\n\n    async rotatePDF(e) {\n        const button = e.target;\n        button.setAttribute(\"disabled\", \"\");\n        const result = await this.props.rotatePDF();\n        if (result) {\n            this.root.querySelector(\"#pageRotateCw\").click();\n            button.removeAttribute(\"disabled\");\n            this.refreshSignItems();\n        }\n    }\n\n    setInitialZoom() {\n        let button = this.root.querySelector(\"button#zoomIn\");\n        if (!this.env.isSmall) {\n            button = this.root.querySelector(\"button#zoomOut\");\n            button.click();\n        }\n        button.click();\n    }\n\n    postRender() {\n        const refreshSignItemsIntervalId = setInterval(() => this.refreshSignItems(), 2000);\n        this.cleanupFns.push(() => clearInterval(refreshSignItemsIntervalId));\n    }\n\n    /**\n     * Creates rendering context for the sign item based on the sign item type\n     * @param {number} typeId\n     * @returns {Object} context\n     */\n    createSignItemDataFromType(typeId) {\n        const type = this.signItemTypesById[typeId];\n        return {\n            required: true,\n            editMode: true,\n            readonly: true,\n            updated: true,\n            responsible: this.currentRole,\n            option_ids: [],\n            options: [],\n            name: type.name,\n            width: type.default_width,\n            height: type.default_height,\n            alignment: \"center\",\n            type: type.item_type,\n            placeholder: type.placeholder,\n            classes: `o_color_responsible_${this.signRolesById[this.currentRole].color}`,\n            style: `width: ${type.default_width * 100}%; height: ${type.default_height * 100}%;`,\n            type_id: [type.id],\n        };\n    }\n\n    /**\n     * @typedef {Object} SignItem\n     * @property {Object} data // sign item data returned from the search_read\n     * @property {HTMLElement} el // html element of the sign item\n     */\n\n    /**\n     * Converts a list of sign items to an object indexed by page and id\n     * @returns { Object.<page:number, Object.<id:number, SignItem >>}\n     */\n    getSignItems() {\n        const signItems = {};\n        for (let currentPage = 1; currentPage <= this.pageCount; currentPage++) {\n            signItems[currentPage] = {};\n        }\n        for (const signItem of this.props.signItems) {\n            signItems[signItem.page][signItem.id] = {\n                data: signItem,\n                el: null,\n            };\n        }\n        return signItems;\n    }\n\n    /**\n     * Gets page container from the page number\n     * @param {Number} page\n     * @returns {HTMLElement} pageContainer\n     */\n    getPageContainer(page) {\n        return this.root.querySelector(`.page[data-page-number=\"${page}\"]`);\n    }\n\n    /**\n     * @returns canvas layer used for drawing radio item connecting lines.\n     */\n    getCanvas() {\n        return this.root.querySelector(\"#canvas_layer_0\");\n    }\n}\n", "/** @odoo-module **/\n\nimport { App, Component, xml, whenReady, useEffect, useComponent } from \"@odoo/owl\";\nimport { MainComponentsContainer } from \"@web/core/main_components_container\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { makeEnv, startServices } from \"@web/env\";\nimport { SignRefusalDialog } from \"@sign/dialogs/dialogs\";\nimport { SignablePDFIframe } from \"./signable_PDF_iframe\";\nimport { buildPDFViewerURL } from \"@sign/components/sign_request/utils\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nfunction datasetFromElements(elements) {\n    return Array.from(elements).map((el) => {\n        return Object.entries(el.dataset).reduce((dataset, [key, value]) => {\n            try {\n                dataset[key] = JSON.parse(value);\n            } catch {\n                dataset[key] = value;\n            }\n            return dataset;\n        }, {});\n    });\n}\n\nexport class Document extends Component {\n    static template = xml`<t t-slot='default'/>`;\n    static props = [\"parent\", \"PDFIframeClass\"];\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.dialog = useService(\"dialog\");\n        this.ui = useService(\"ui\");\n        this.signInfo = useService(\"signInfo\");\n        useEffect(\n            () => {\n                this.getDataFromHTML();\n                this.signInfo.set({\n                    documentId: this.requestID,\n                    signRequestToken: this.requestToken,\n                    signRequestState: this.requestState,\n                    signRequestItemToken: this.accessToken,\n                    todayFormattedDate: this.todayFormattedDate,\n                });\n            },\n            () => []\n        );\n    }\n\n    getDataFromHTML() {\n        const { el: parentEl } = this.props.parent;\n        this.attachmentLocation = parentEl.querySelector(\n            \"#o_sign_input_attachment_location\"\n        )?.value;\n        this.templateName = parentEl.querySelector(\"#o_sign_input_template_name\")?.value;\n        this.templateID = parseInt(parentEl.querySelector(\"#o_sign_input_template_id\")?.value);\n        this.templateItemsInProgress = parseInt(\n            parentEl.querySelector(\"#o_sign_input_template_in_progress_count\")?.value\n        );\n        this.requestID = parseInt(parentEl.querySelector(\"#o_sign_input_sign_request_id\")?.value);\n        this.requestToken = parentEl.querySelector(\"#o_sign_input_sign_request_token\")?.value;\n        this.requestState = parentEl.querySelector(\"#o_sign_input_sign_request_state\")?.value;\n        this.accessToken = parentEl.querySelector(\"#o_sign_input_access_token\")?.value;\n        this.todayFormattedDate = parentEl.querySelector(\"#o_sign_input_today_formatted_date\")?.value;\n        this.templateEditable = Boolean(parentEl.querySelector(\"#o_sign_input_template_editable\"));\n        this.authMethod = parentEl.querySelector(\"#o_sign_input_auth_method\")?.value;\n        this.signerName = parentEl.querySelector(\"#o_sign_signer_name_input_info\")?.value;\n        this.signerPhone = parentEl.querySelector(\"#o_sign_signer_phone_input_info\")?.value;\n        this.redirectURL = parentEl.querySelector(\"#o_sign_input_optional_redirect_url\")?.value;\n        this.redirectURLText = parentEl.querySelector(\n            \"#o_sign_input_optional_redirect_url\"\n        )?.value;\n        this.redirectURLText = parentEl.querySelector(\n            \"#o_sign_input_optional_redirect_url_text\"\n        )?.value;\n        this.types = datasetFromElements(\n            parentEl.querySelectorAll(\".o_sign_field_type_input_info\")\n        );\n        this.items = datasetFromElements(parentEl.querySelectorAll(\".o_sign_item_input_info\"));\n        this.selectOptions = datasetFromElements(\n            parentEl.querySelectorAll(\".o_sign_select_options_input_info\")\n        );\n        this.validateBanner = parentEl.querySelector(\".o_sign_validate_banner\");\n        this.validateButton = parentEl.querySelector(\".o_sign_validate_banner button\");\n        this.validateButtonText = this.validateButton?.textContent;\n        this.currentRole = parseInt(parentEl.querySelector(\"#o_sign_input_current_role\")?.value);\n        this.currentName = parentEl.querySelector(\"#o_sign_input_current_role_name\")?.value;\n\n        this.isUnknownPublicUser = Boolean(parentEl.querySelector(\"#o_sign_is_public_user\"));\n        this.frameHash = parentEl.querySelector(\"#o_sign_input_sign_frame_hash\")?.value;\n        this.PDFIframe = parentEl.querySelector(\"iframe.o_sign_pdf_iframe\");\n        this.PDFIframe.setAttribute(\n            \"src\",\n            buildPDFViewerURL(this.attachmentLocation, this.env.isSmall)\n        );\n        this.PDFIframe.onload = () => {\n            setTimeout(() => this.initializeIframe(), 1);\n        };\n    }\n    initializeIframe() {\n        this.iframe = new this.props.PDFIframeClass(\n            this.PDFIframe.contentDocument,\n            this.env,\n            {\n                rpc,\n                orm: this.orm,\n                dialog: this.dialog,\n                ui: this.ui,\n                signInfo: this.signInfo,\n            },\n            this.iframeProps\n        );\n    }\n\n    get iframeProps() {\n        return {\n            attachmentLocation: this.attachmentLocation,\n            requestID: this.requestID,\n            requestToken: this.requestToken,\n            accessToken: this.accessToken,\n            signItemTypes: this.types,\n            signItems: this.items,\n            hasSignRequests: false,\n            signItemOptions: this.selectOptions,\n            currentRole: this.currentRole,\n            currentName: this.currentName,\n            readonly: this.PDFIframe.getAttribute(\"readonly\") === \"readonly\",\n            frameHash: this.frameHash,\n            signerName: this.signerName,\n            signerPhone: this.signerPhone,\n            validateBanner: this.validateBanner,\n            validateButton: this.validateButton,\n            validateButtonText: this.validateButtonText,\n            isUnknownPublicUser: this.isUnknownPublicUser,\n            authMethod: this.authMethod,\n            redirectURL: this.redirectURL,\n            redirectURLText: this.redirectURLText,\n            templateEditable: this.templateEditable,\n        };\n    }\n}\n\nfunction usePublicRefuseButton() {\n    const component = useComponent();\n    useEffect(\n        () => {\n            const refuseButtons = document.querySelectorAll(\".o_sign_refuse_document_button\");\n            if (refuseButtons) {\n                refuseButtons.forEach((button) =>\n                    button.addEventListener(\"click\", () => {\n                        component.dialog.add(SignRefusalDialog);\n                    })\n                );\n            }\n        },\n        () => []\n    );\n}\n\nexport class SignableDocument extends Document {\n    static components = {\n        MainComponentsContainer,\n    };\n    static template = xml`<MainComponentsContainer/>`;\n\n    setup() {\n        super.setup();\n        this.coords = {};\n        usePublicRefuseButton();\n        useEffect(\n            () => {\n                if (this.requestID) {\n                    // Geolocation\n                    const { el: parentEl } = this.props.parent;\n                    const askLocation = parentEl.getElementById(\n                        \"o_sign_ask_location_input\"\n                    );\n                    if (askLocation && navigator.geolocation) {\n                        navigator.geolocation.getCurrentPosition(\n                            ({ coords: { latitude, longitude } }) => {\n                                Object.assign(this.coords, {\n                                    latitude,\n                                    longitude,\n                                });\n                                if (this.requestState !== \"shared\") {\n                                    rpc(\n                                        `/sign/save_location/${this.requestID}/${this.accessToken}`,\n                                        this.coords\n                                    );\n                                }\n                            }\n                        , () => {}, {enableHighAccuracy: true}\n                        );\n                    }\n                }\n            },\n            () => [this.requestID]\n        );\n    }\n\n    get iframeProps() {\n        return {\n            ...super.iframeProps,\n            coords: this.coords,\n        };\n    }\n}\n\n/**\n * Mounts the SignableComponent\n * @param { HTMLElement } parent\n */\nexport async function initDocumentToSign(parent) {\n    // Manually add 'sign' to module list and load the translations\n    const env = makeEnv();\n    await startServices(env);\n    await whenReady();\n    const app = new App(SignableDocument, {\n        name: \"Signable Document\",\n        env,\n        props: {\n            parent: {el: parent},\n            PDFIframeClass: SignablePDFIframe },\n        getTemplate,\n        dev: env.debug,\n        translatableAttributes: [\"data-tooltip\"],\n        translateFn: _t,\n    });\n    await app.mount(parent.body);\n}\n", "/** @odoo-module **/\n\nimport { renderToString } from \"@web/core/utils/render\";\n\nexport class MobileInputBottomSheet {\n    constructor(options) {\n        this.type = options.type || \"text\";\n        this.placeholder = options.placeholder || \"\";\n        this.label = options.label || this.placeholder;\n        this.value = options.value || \"\";\n        this.buttonText = options.buttonText;\n        this.element = options.element;\n        this.onTextChange = options.onTextChange || function () {};\n        this.onValidate = options.onValidate || function () {};\n\n        document.body.insertAdjacentHTML(\n            \"beforeend\",\n            renderToString(\"sign.MobileInputBottomSheet\", this)\n        );\n        this.el = document.body.lastChild;\n        this.registerEvents();\n    }\n\n    registerEvents() {\n        const field = this.el.querySelector(\".o_sign_item_bottom_sheet_field\");\n        const nextButton = this.el.querySelector(\".o_sign_next_button\");\n\n        if (field) {\n            field.addEventListener(\"blur\", () => {\n                this._onBlurField();\n            });\n            field.addEventListener(\"keyup\", () => {\n                this._onKeyUpField();\n            });\n        }\n\n        if (nextButton) {\n            nextButton.addEventListener(\"click\", () => {\n                this._onClickNext();\n            });\n        }\n    }\n\n    updateInputText(text) {\n        this.value = text;\n        this.el.querySelector(\".o_sign_item_bottom_sheet_field\").value = text;\n        this._toggleButton();\n    }\n\n    show() {\n        // hide previous bottom sheet\n        const bottomSheet = document.querySelector(\".o_sign_item_bottom_sheet.show\");\n        if (bottomSheet) {\n            bottomSheet.classList.remove(\"show\");\n        }\n\n        this._toggleButton();\n        setTimeout(() => this.el.classList.add(\"show\"));\n        this.el.querySelector(\".o_sign_item_bottom_sheet_field\").focus();\n    }\n\n    hide() {\n        this.el.classList.remove(\"show\");\n        this.el.addEventListener(\"transitionend\", () => (this.el.style.display = \"none\"), {\n            once: true,\n        });\n    }\n\n    _toggleButton() {\n        const buttonNext = this.el.querySelector(\".o_sign_next_button\");\n        this.value.length\n            ? buttonNext.removeAttribute(\"disabled\")\n            : buttonNext.setAttribute(\"disabled\", \"disabled\");\n    }\n\n    _updateText() {\n        this.value = this.el.querySelector(\".o_sign_item_bottom_sheet_field\").value;\n        this.onTextChange(this.value);\n        this._toggleButton();\n    }\n\n    _onBlurField() {\n        this._updateText();\n    }\n\n    _onClickNext() {\n        this.onValidate(this.value);\n    }\n\n    _onKeyUpField() {\n        this._updateText();\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { offset } from \"./utils\";\n\n/**\n * Starts the sign item navigator\n * @param { SignablePDFIframe } parent\n * @param { HTMLElement } target\n * @param { Object } types\n * @param { Environment } env\n */\nexport function startSignItemNavigator(parent, target, types, env) {\n    function setTip(text) {\n        navigator.innerText = text;\n    }\n\n    const state = {\n        started: false,\n        isScrolling: false,\n    };\n\n    const navigator = document.createElement(\"div\");\n    navigator.classList.add(\"o_sign_sign_item_navigator\");\n    navigator.addEventListener(\"click\", goToNextSignItem);\n    target.append(navigator);\n\n    const navLine = document.createElement(\"div\");\n    navLine.classList.add(\"o_sign_sign_item_navline\");\n    navigator.before(navLine);\n\n    setTip(_t(\"Click to start\"));\n    navigator.focus();\n\n    function goToNextSignItem() {\n        if (!state.started) {\n            state.started = true;\n            parent.refreshSignItems();\n            goToNextSignItem();\n            return false;\n        }\n        const selectedElements = target.querySelectorAll(\".ui-selected\");\n        selectedElements.forEach((selectedElement) => {\n            selectedElement.classList.remove(\"ui-selected\");\n        });\n        const signItemsToComplete = parent.checkSignItemsCompletion().sort((a, b) => {\n            return (\n                100 * (a.data.page - b.data.page) +\n                10 * (a.data.posY - b.data.posY) +\n                (a.data.posX - b.data.posX)\n            );\n        });\n        if (signItemsToComplete.length > 0) {\n            scrollToSignItem(signItemsToComplete[0]);\n        }\n    }\n\n    /**\n     * Sets the entire radio set on focus.\n     * @param {Number} radio_set_id \n     */\n    function highligtRadioSet(radio_set_id) {\n        parent.checkSignItemsCompletion().filter((item) => item.data.radio_set_id === radio_set_id).forEach(item => {\n            item.el.classList.add(\"ui-selected\");\n        });\n    }\n\n    function scrollToSignItem({ el: item, data }) {\n        _scrollToSignItemPromise(item).then(() => {\n            const type = types[data.type_id];\n            if (type.item_type === \"text\" && item.querySelector(\"input\")) {\n                item.value = item.querySelector(\"input\").value;\n                item.focus = () => item.querySelector(\"input\").focus();\n            }\n            // maybe store signature in data rather than in the dataset\n            if (item.value === \"\" && !item.dataset.signature) {\n                setTip(type.tip);\n            }\n            parent.refreshSignItems();\n            if (data.type === \"radio\") {\n                //we need to highligt the entire radio set items\n                highligtRadioSet(data.radio_set_id);                \n            } else {\n                item.focus();\n                item.classList.add(\"ui-selected\");\n            }\n            if ([\"signature\", \"initial\"].includes(type.item_type)) {\n                if (item.dataset.hasFocus) {\n                    const clickableElement = data.isSignItemEditable\n                        ? item.querySelector(\".o_sign_item_display\")\n                        : item;\n                    clickableElement.click();\n                } else {\n                    item.dataset.hasFocus = true;\n                }\n            }\n            state.isScrolling = false;\n        });\n    }\n\n    function _scrollToSignItemPromise(item) {\n        if (env.isSmall) {\n            return new Promise((resolve) => {\n                state.isScrolling = true;\n                item.scrollIntoView({\n                    behavior: \"smooth\",\n                    block: \"center\",\n                    inline: \"center\",\n                });\n                resolve();\n            });\n        }\n        state.isScrolling = true;\n        const viewer = target.querySelector(\"#viewer\");\n        const containerHeight = target.offsetHeight;\n        const viewerHeight = viewer.offsetHeight;\n\n        let scrollOffset = containerHeight / 4;\n        const scrollTop = offset(item).top - offset(viewer).top - scrollOffset;\n        if (scrollTop + containerHeight > viewerHeight) {\n            scrollOffset += scrollTop + containerHeight - viewerHeight;\n        }\n        if (scrollTop < 0) {\n            scrollOffset += scrollTop;\n        }\n        scrollOffset +=\n            offset(target).top -\n            navigator.offsetHeight / 2 +\n            item.getBoundingClientRect().height / 2;\n\n        const duration = Math.max(\n            Math.min(\n                500,\n                5 *\n                    (Math.abs(target.scrollTop - scrollTop) +\n                        Math.abs(navigator.getBoundingClientRect().top) -\n                        scrollOffset)\n            ),\n            100\n        );\n\n        return new Promise((resolve, reject) => {\n            target.scrollTo({ top: scrollTop, behavior: \"smooth\" });\n            const an = navigator.animate(\n                { top: `${scrollOffset}px` },\n                { duration, fill: \"forwards\" }\n            );\n            const an2 = navLine.animate(\n                { top: `${scrollOffset}px` },\n                { duration, fill: \"forwards\" }\n            );\n            Promise.all([an.finished, an2.finished]).then(() => resolve());\n        });\n    }\n\n    function toggle(force) {\n        navigator.style.display = force ? \"\" : \"none\";\n        navLine.style.display = force ? \"\" : \"none\";\n    }\n\n    return {\n        setTip,\n        goToNextSignItem,\n        toggle,\n        state,\n    };\n}\n", "/** @odoo-module **/\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { PDFIframe } from \"./PDF_iframe\";\nimport { startSignItemNavigator } from \"./sign_item_navigator\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { MobileInputBottomSheet } from \"@sign/components/sign_request/mobile_input_bottom_sheet\";\nimport {\n    SignNameAndSignatureDialog,\n    ThankYouDialog,\n    PublicSignerDialog,\n    SMSSignerDialog,\n    NextDirectSignDialog,\n} from \"@sign/dialogs/dialogs\";\n\nexport class SignablePDFIframe extends PDFIframe {\n    /**\n     * Renders custom elements inside the PDF.js iframe when signing\n     * @param {HTMLIFrameElement} iframe\n     * @param {Document} root\n     * @param {Object} env\n     * @param {Object} owlServices\n     * @param {Object} props\n     */\n    constructor(root, env, owlServices, props) {\n        super(root, env, owlServices, props);\n        this.currentRole = this.props.currentRole;\n        this.currentRoleName = this.props.currentName;\n        this.signerName = props.signerName;\n        this.frameHash =\n            (this.props.frameHash && this.props.frameHash.substring(0, 10) + \"...\") || \"\";\n\n        this.radioSets = {};\n        this.props.signItems.forEach((item) => {\n            if(item.radio_set_id) {\n                if (item.radio_set_id in this.radioSets) {\n                    this.radioSets[item.radio_set_id].items.push(item);\n                } else {\n                    this.radioSets[item.radio_set_id] = {\n                        selected: null,\n                        items: [item],\n                    }\n                }\n            }\n        })\n\n        for (const radio_set_id in this.radioSets) {\n            this.radioSets[radio_set_id].items = this.radioSets[radio_set_id].items.sort((a, b) => {\n                return (\n                    100 * (a.page - b.page) +\n                    10 * (a.posY - b.posY) +\n                    (a.posX - b.posX)\n                );\n            });\n        }\n    }\n\n    getSignItemById(id) {\n        for (const page in this.signItems) {\n            if (this.signItems[page].hasOwnProperty(id)) {\n                return this.signItems[page][id];\n            }\n        }\n        return undefined;\n    }\n\n    /**\n     * Modify the selected sign item of the corresponding radio set.\n     * @param {SignItem} signItem \n     */\n    handleRadioItemSelected(signItem) {\n        const radio_set_id = signItem.data.radio_set_id;\n        if (this.radioSets[radio_set_id].selected !== signItem.data.id) {\n            this.radioSets[signItem.data.radio_set_id].selected = signItem.data.id;\n        }else if (!signItem.data.required) {\n            signItem.el.checked = false;\n            this.radioSets[radio_set_id].selected = undefined;\n        }\n    }\n\n    enableCustom(signItem) {\n        if (this.readonly || signItem.data.responsible !== this.currentRole) {\n            return;\n        }\n        const signItemElement = signItem.el;\n        const signItemData = signItem.data;\n        const signItemType = this.signItemTypesById[signItemData.type_id];\n        const { name, item_type: type, auto_value: autoValue } = signItemType;\n        if (name === _t(\"Date\")) {\n            signItemElement.addEventListener(\"focus\", (e) => {\n                this.fillTextSignItem(e.currentTarget, this.signInfo.get('todayFormattedDate'));\n            });\n        } else if (type === \"signature\" || type === \"initial\") {\n            signItemElement.addEventListener(\"click\", (e) => {\n                this.handleSignatureDialogClick(e.currentTarget, signItemType);\n            });\n        } else if (type === \"radio\") {\n            signItemElement.addEventListener(\"click\", (e) => {\n                this.handleRadioItemSelected(signItem);\n            })\n        }\n\n        if (autoValue && [\"text\", \"textarea\"].includes(type)) {\n            signItemElement.addEventListener(\"focus\", (e) => {\n                this.fillTextSignItem(e.currentTarget, autoValue);\n            });\n        }\n\n        if (this.env.isSmall && [\"text\", \"textarea\"].includes(type)) {\n            const inputBottomSheet = new MobileInputBottomSheet({\n                type: type,\n                element: signItemElement,\n                value: signItemElement.value,\n                label: `${signItemType.tip}: ${signItemType.placeholder}`,\n                placeholder: signItemElement.placeholder,\n                onTextChange: (value) => {\n                    signItemElement.value = value;\n                },\n                onValidate: (value) => {\n                    signItemElement.value = value;\n                    signItemElement.dispatchEvent(new Event(\"input\", { bubbles: true }));\n                    inputBottomSheet.hide();\n                    this.navigator.goToNextSignItem();\n                },\n                buttonText: _t(\"next\"),\n            });\n\n            signItemElement.addEventListener(\"focus\", () => {\n                inputBottomSheet.updateInputText(signItemElement.value);\n                inputBottomSheet.show();\n            });\n        }\n\n        if (type === \"selection\") {\n            if (signItemElement.value) {\n                this.handleInput();\n            }\n            const optionDiv = signItemElement.querySelector(\".o_sign_select_options_display\");\n            optionDiv.addEventListener(\"click\", (e) => {\n                if (e.target.classList.contains(\"o_sign_item_option\")) {\n                    const option = e.target;\n                    const selectedValue = option.dataset.id;\n                    signItemElement.value = selectedValue;\n                    option.classList.add(\"o_sign_selected_option\");\n                    option.classList.remove(\"o_sign_not_selected_option\");\n                    const notSelected = optionDiv.querySelectorAll(\n                        `.o_sign_item_option:not([data-id='${selectedValue}'])`\n                    );\n                    [...notSelected].forEach((el) => {\n                        el.classList.remove(\"o_sign_selected_option\");\n                        el.classList.add(\"o_sign_not_selected_option\");\n                    });\n                    this.handleInput();\n                }\n            });\n        }\n\n        signItemElement.addEventListener(\"input\", this.handleInput.bind(this));\n    }\n\n    handleInput() {\n        this.checkSignItemsCompletion();\n        this.navigator.setTip(_t(\"next\"));\n    }\n\n    /**\n     * Logic for wizard/mark behavior is:\n     * If auto_value is defined and the item is not marked yet, auto_value is used\n     * Else, wizard is opened.\n     * @param { HTMLElement } signatureItem\n     * @param { Object } type\n     */\n    handleSignatureDialogClick(signatureItem, signItemType) {\n        this.refreshSignItems();\n        const signature = signatureItem.dataset.signature;\n        const { auto_value: autoValue, frame_value: frameValue, item_type: type } = signItemType;\n        if (autoValue && !signature) {\n            Promise.all([\n                this.adjustSignatureSize(autoValue, signatureItem),\n                this.adjustSignatureSize(frameValue, signatureItem),\n            ]).then(([data, frameData]) => {\n                this.fillItemWithSignature(signatureItem, data, { frame: frameData, hash: \"0\" });\n                this.handleInput();\n            });\n        } else if (type === \"initial\" && this.nextInitial && !signature) {\n            this.adjustSignatureSize(this.nextInitial, signatureItem).then((data) => {\n                this.fillItemWithSignature(signatureItem, data);\n                this.handleInput();\n            });\n        } else {\n            this.openSignatureDialog(signatureItem, signItemType);\n        }\n    }\n\n    fillTextSignItem(signItemElement, value) {\n        if (signItemElement.value === \"\") {\n            signItemElement.value = value;\n            this.handleInput();\n        }\n    }\n\n    /**\n     * Adjusts signature/initial size to fill the dimensions of the sign item box\n     * @param { String } data base64 image\n     * @param { HTMLElement } signatureItem\n     * @returns { Promise }\n     */\n    adjustSignatureSize(data, signatureItem) {\n        if (!data) {\n            return Promise.resolve(false);\n        }\n        return new Promise((resolve, reject) => {\n            const img = new Image();\n            img.onload = () => {\n                const c = document.createElement(\"canvas\");\n                if (\n                    !signatureItem.parentElement ||\n                    !signatureItem.parentElement.classList.contains(\"page\")\n                ) {\n                    // checks if element is detached from pdf js\n                    this.refreshSignItems();\n                }\n                const { width: boxWidth, height: boxHeight } =\n                    signatureItem.getBoundingClientRect();\n                const imgHeight = img.height;\n                const imgWidth = img.width;\n                const ratioBoxWidthHeight = boxWidth / boxHeight;\n                const ratioImageWidthHeight = imgWidth / imgHeight;\n\n                const [canvasHeight, canvasWidth] =\n                    ratioBoxWidthHeight > ratioImageWidthHeight\n                        ? [imgHeight, imgHeight * ratioBoxWidthHeight]\n                        : [imgWidth / ratioBoxWidthHeight, imgWidth];\n\n                c.height = canvasHeight;\n                c.width = canvasWidth;\n\n                const ctx = c.getContext(\"2d\");\n                const oldShadowColor = ctx.shadowColor;\n                ctx.shadowColor = \"transparent\";\n                ctx.drawImage(\n                    img,\n                    c.width / 2 - img.width / 2,\n                    c.height / 2 - img.height / 2,\n                    img.width,\n                    img.height\n                );\n                ctx.shadowColor = oldShadowColor;\n                resolve(c.toDataURL());\n            };\n            img.src = data;\n        });\n    }\n\n    fillItemWithSignature(signatureItem, image, frameData = false) {\n        signatureItem.dataset.signature = image;\n        signatureItem.replaceChildren();\n        const signHelperSpan = document.createElement(\"span\");\n        signHelperSpan.classList.add(\"o_sign_helper\");\n        signatureItem.append(signHelperSpan);\n        if (frameData && frameData.frame) {\n            signatureItem.dataset.frameHash = frameData.hash;\n            signatureItem.dataset.frame = frameData.frame;\n            const frameImage = document.createElement(\"img\");\n            frameImage.src = frameData.frame;\n            frameImage.classList.add(\"o_sign_frame\");\n            signatureItem.append(frameImage);\n        } else {\n            delete signatureItem.dataset.frame;\n        }\n        const signatureImage = document.createElement(\"img\");\n        signatureImage.src = image;\n        signatureItem.append(signatureImage);\n    }\n\n    closeDialog() {\n        this.closeFn && this.closeFn();\n        this.closeFn = false;\n    }\n\n    /**\n     * Opens the signature dialog\n     * @param { HTMLElement } signatureItem\n     * @param {*} type\n     */\n    openSignatureDialog(signatureItem, type) {\n        if (this.dialogOpen) {\n            return;\n        }\n        const signature = {\n            name: this.signerName || \"\",\n        };\n        const frame = {};\n        const { height, width } = signatureItem.getBoundingClientRect();\n        const signFrame = signatureItem.querySelector(\".o_sign_frame\");\n        this.dialogOpen = true;\n        // If we already have an image, we propagate it to populate the \"draw\" tab\n        const signatureImage = signatureItem?.dataset?.signature;\n        const signMode = type.auto_value ? \"draw\" : \"auto\"\n        if (signMode == \"draw\" && signatureImage) {\n            signature.signatureImage = signatureImage;\n        }\n        this.closeFn = this.dialog.add(\n            SignNameAndSignatureDialog,\n            {\n                frame,\n                signature,\n                signatureType: type.item_type,\n                displaySignatureRatio: width / height,\n                activeFrame: Boolean(signFrame) || !type.auto_value,\n                mode: signMode,\n                defaultFrame: type.frame_value || \"\",\n                hash: this.frameHash,\n                signatureImage,\n                onConfirm: async () => {\n                    if (!signature.isSignatureEmpty && signature.signatureChanged) {\n                        const signatureName = signature.name;\n                        this.signerName = signatureName;\n                        await frame.updateFrame();\n                        const frameData = frame.getFrameImageSrc();\n                        const signatureSrc = signature.getSignatureImage();\n                        type.auto_value = signatureSrc;\n                        type.frame_value = frameData;\n                        if (user.userId) {\n                            await this.updateUserSignature(type);\n                        }\n                        this.fillItemWithSignature(signatureItem, signatureSrc, {\n                            frame: frameData,\n                            hash: this.frameHash,\n                        });\n                    } else if (signature.signatureChanged) {\n                        // resets the sign item\n                        delete signatureItem.dataset.signature;\n                        delete signatureItem.dataset.frame;\n                        signatureItem.replaceChildren();\n                        const signHelperSpan = document.createElement(\"span\");\n                        signHelperSpan.classList.add(\"o_sign_helper\");\n                        signatureItem.append(signHelperSpan);\n                        if (type.placeholder) {\n                            const placeholderSpan = document.createElement(\"span\");\n                            placeholderSpan.classList.add(\"o_placeholder\");\n                            placeholderSpan.innerText = type.placeholder;\n                            signatureItem.append(placeholderSpan);\n                        }\n                    }\n                    this.closeDialog();\n                    this.handleInput();\n                },\n                onConfirmAll: async () => {\n                    const signatureName = signature.name;\n                    this.signerName = signatureName;\n                    await frame.updateFrame();\n                    const frameData = frame.getFrameImageSrc();\n                    const signatureSrc = signature.getSignatureImage();\n                    type.auto_value = signatureSrc;\n                    type.frame_value = frameData;\n                    if (user.userId) {\n                        await this.updateUserSignature(type);\n                    }\n                    for (const page in this.signItems) {\n                        await Promise.all(\n                            Object.values(this.signItems[page]).reduce((promiseList, signItem) => {\n                                if (\n                                    signItem.data.responsible === this.currentRole &&\n                                    signItem.data.type_id === type.id\n                                ) {\n                                    promiseList.push(\n                                        Promise.all([\n                                            this.adjustSignatureSize(signatureSrc, signItem.el),\n                                            this.adjustSignatureSize(frameData, signItem.el),\n                                        ]).then(([data, frameData]) => {\n                                            this.fillItemWithSignature(signItem.el, data, {\n                                                frame: frameData,\n                                                hash: this.frameHash,\n                                            });\n                                        })\n                                    );\n                                }\n                                return promiseList;\n                            }, [])\n                        );\n                    }\n                    this.closeDialog();\n                    this.handleInput();\n                },\n                onCancel: () => {\n                    this.closeDialog();\n                },\n            },\n            {\n                onClose: () => {\n                    this.dialogOpen = false;\n                },\n            }\n        );\n    }\n\n    checkSignItemsCompletion() {\n        this.refreshSignItems();\n        const itemsToSign = [];\n        for (const page in this.signItems) {\n            Object.values(this.signItems[page]).forEach((signItem) => {\n                if (\n                    signItem.data.required &&\n                    signItem.data.responsible === this.currentRole &&\n                    !signItem.data.value\n                ) {\n                    if(signItem.data.type === \"radio\" && this.radioSets[signItem.data.radio_set_id].selected){\n                        return;\n                    }\n                    const el =\n                        signItem.data.isEditMode && signItem.el.type === \"text\"\n                            ? el.querySelector(\"input\")\n                            : signItem.el;\n                    const uncheckedBox = el.value === \"on\" && !el.checked;\n                    if (!((el.value && el.value.trim()) || el.dataset.signature) || uncheckedBox) {\n                        itemsToSign.push(signItem);\n                    }\n                }\n            });\n        }\n\n        itemsToSign.length ? this.hideBanner() : this.showBanner();\n        this.navigator.toggle(itemsToSign.length > 0);\n        return itemsToSign;\n    }\n\n    showBanner() {\n        this.props.validateBanner.style.display = \"block\";\n        const an = this.props.validateBanner.animate(\n            { opacity: 1 },\n            { duration: 500, fill: \"forwards\" }\n        );\n        an.finished.then(() => {\n            if (this.env.isSmall) {\n                this.props.validateBanner.scrollIntoView({\n                    behavior: \"smooth\",\n                    block: \"center\",\n                    inline: \"center\",\n                });\n            }\n        });\n    }\n\n    hideBanner() {\n        this.props.validateBanner.style.display = \"none\";\n        this.props.validateBanner.style.opacity = 0;\n    }\n\n    /**\n     * Updates the user's signature in the res.user model\n     * @param { Object } type\n     */\n    updateUserSignature(type) {\n        return rpc(\"/sign/update_user_signature\", {\n            sign_request_id: this.props.requestID,\n            role: this.currentRole,\n            signature_type: type.item_type === \"signature\" ? \"sign_signature\" : \"sign_initials\",\n            datas: type.auto_value,\n            frame_datas: type.frame_value,\n        });\n    }\n\n    /**\n     * Extends the rendering context of the sign item based on its data\n     * @param {SignItem.data} signItem\n     * @returns {Object}\n     */\n    getContext(signItem) {\n        return super.getContext(signItem);\n    }\n\n    /**\n     * Hook executed before rendering the sign items and the sidebar\n     */\n    preRender() {\n        super.preRender();\n    }\n\n    postRender() {\n        super.postRender();\n        if (this.readonly) {\n            return;\n        }\n        this.navigator = startSignItemNavigator(\n            this,\n            this.root.querySelector(\"#viewerContainer\"),\n            this.signItemTypesById,\n            this.env\n        );\n        this.checkSignItemsCompletion();\n\n        this.root.querySelector(\"#viewerContainer\").addEventListener(\"scroll\", () => {\n            if (!this.navigator.state.isScrolling && this.navigator.state.started) {\n                this.navigator.setTip(_t(\"next\"));\n            }\n        });\n\n        this.root.querySelector(\"#viewerContainer\").addEventListener(\"keydown\", (e) => {\n            if (e.key !== \"Enter\" || (e.target.tagName.toLowerCase() === 'textarea')) {\n                return;\n            }\n            this.navigator.goToNextSignItem();\n        });\n\n        this.props.validateBanner\n            .querySelector(\".o_validate_button\")\n            .addEventListener(\"click\", () => {\n                this.signDocument();\n            });\n    }\n\n    getMailFromSignItems() {\n        let mail = \"\";\n        for (const page in this.signItems) {\n            Object.values(this.signItems[page]).forEach(({ el }) => {\n                const childInput = el.querySelector(\"input\");\n                const value = el.value || (childInput && childInput.value);\n                if (value && value.indexOf(\"@\") >= 0) {\n                    mail = value;\n                }\n            });\n        }\n        return mail;\n    }\n\n    signDocument() {\n        this.props.validateBanner.setAttribute(\"disabled\", true);\n        this.signatureInfo = { name: this.signerName || \"\", mail: this.getMailFromSignItems() };\n\n        [\n            this.signatureInfo.signatureValues,\n            this.signatureInfo.frameValues,\n            this.signatureInfo.newSignItems,\n        ] = this.getSignatureValuesFromConfiguration();\n        if (!this.signatureInfo.signatureValues) {\n            this.checkSignItemsCompletion();\n            this.dialog.add(AlertDialog, {\n                title: _t(\"Warning\"),\n                body: _t(\"Some fields have still to be completed\"),\n            });\n            this.props.validateButton.textContent = this.props.validateButtonText;\n            this.props.validateBanner.removeAttribute(\"disabled\");\n            return;\n        }\n        this.signatureInfo.hasNoSignature =\n            Object.keys(this.signatureInfo.signatureValues).length == 0 &&\n            Object.keys(this.signItems).length == 0;\n        this._signDocument();\n    }\n\n    async _signDocument() {\n        this.props.validateButton.textContent = this.props.validateButtonText;\n        this.props.validateButton.setAttribute(\"disabled\", true);\n        if (this.signatureInfo.hasNoSignature) {\n            const signature = {\n                name: this.signerName || \"\",\n            };\n            this.closeFn = this.dialog.add(SignNameAndSignatureDialog, {\n                signature,\n                onConfirm: () => {\n                    this.signatureInfo.name = signature.name;\n                    this.signatureInfo.signatureValues = signature\n                        .getSignatureImage()\n                        .split(\",\")[1];\n                    this.signatureInfo.frameValues = [];\n                    this.signatureInfo.hasNoSignature = false;\n                    this.closeDialog();\n                    this._signDocument();\n                },\n                onCancel: () => {\n                    this.closeDialog();\n                },\n            });\n        } else if (this.props.isUnknownPublicUser) {\n            this.closeFn = this.dialog.add(\n                PublicSignerDialog,\n                {\n                    name: this.signatureInfo.name,\n                    mail: this.signatureInfo.mail,\n                    postValidation: async (requestID, requestToken, accessToken) => {\n                        this.signInfo.set({\n                            documentId: requestID,\n                            signRequestToken: requestToken,\n                            signRequestItemToken: accessToken,\n                        });\n                        this.props.requestID = requestID;\n                        this.props.requestToken = requestToken;\n                        this.props.accessToken = accessToken;\n                        if (this.props.coords) {\n                            await rpc(\n                                `/sign/save_location/${requestID}/${accessToken}`,\n                                this.props.coords\n                            );\n                        }\n                        this.props.isUnknownPublicUser = false;\n                        this._signDocument();\n                    },\n                },\n                {\n                    onClose: () => {\n                        this.props.validateButton.removeAttribute(\"disabled\");\n                    },\n                }\n            );\n        } else if (this.props.authMethod) {\n            this.openAuthDialog();\n        } else {\n            this._sign();\n        }\n    }\n\n    _getRouteAndParams() {\n        const route = this.signatureInfo.smsToken\n            ? `/sign/sign/${encodeURIComponent(this.props.requestID)}/${encodeURIComponent(\n                  this.props.accessToken\n              )}/${encodeURIComponent(this.signatureInfo.smsToken)}`\n            : `/sign/sign/${encodeURIComponent(this.props.requestID)}/${encodeURIComponent(\n                  this.props.accessToken\n              )}`;\n\n        const params = {\n            signature: this.signatureInfo.signatureValues,\n            frame: this.signatureInfo.frameValues,\n            new_sign_items: this.signatureInfo.newSignItems,\n        };\n\n        return [route, params];\n    }\n\n    async _sign() {\n        const [route, params] = this._getRouteAndParams();\n        this.ui.block();\n        const response = await rpc(route, params).finally(() => this.ui.unblock());\n        this.props.validateButton.textContent = this.props.validateButtonText;\n        this.props.validateButton.removeAttribute(\"disabled\");\n        if (response.success) {\n            if (response.url) {\n                document.location.pathname = response.url;\n            } else {\n                this.disableItems();\n                // only available in backend\n                const nameList = this.signInfo.get(\"nameList\");\n                if (nameList && nameList.length > 0) {\n                    this.dialog.add(NextDirectSignDialog);\n                } else {\n                    this.openThankYouDialog();\n                }\n            }\n            this.hideBanner();\n        } else {\n            if (response.sms) {\n                this.dialog.add(AlertDialog, {\n                    title: _t(\"Error\"),\n                    body: _t(\n                        \"Your signature was not submitted. Ensure the SMS validation code is correct.\"\n                    ),\n                });\n            } else {\n                this.dialog.add(\n                    AlertDialog,\n                    {\n                        title: _t(\"Error\"),\n                        body: _t(\n                            \"Sorry, an error occurred, please try to fill the document again.\"\n                        ),\n                    },\n                    {\n                        onClose: () => {\n                            window.location.reload();\n                        },\n                    }\n                );\n            }\n            this.props.validateButton.setAttribute(\"disabled\", true);\n        }\n    }\n\n    /**\n     * Gets the signature values from the sign items\n     * Gets the frame values\n     * Gets the sign items that were added in edit while signing\n     * @returns { Array } [signature values, frame values, added sign items]\n     */\n    getSignatureValuesFromConfiguration() {\n        const signatureValues = {};\n        const frameValues = {};\n        const newSignItems = {};\n        for (const page in this.signItems) {\n            for (const item of Object.values(this.signItems[page])) {\n                const responsible = item.data.responsible || 0;\n                if (responsible > 0 && responsible !== this.currentRole) {\n                    continue;\n                }\n\n                const value = this.getSignatureValueFromElement(item);\n                const [frameValue, frameHash] = item.el.dataset.signature\n                    ? [item.el.dataset.frame, item.el.dataset.frameHash]\n                    : [false, false];\n\n                if (!value) {\n                    if (item.data.required) {\n                        return [{}, {}];\n                    }\n                    continue;\n                }\n\n                signatureValues[item.data.id] = value;\n                frameValues[item.data.id] = { frameValue, frameHash };\n                if (item.data.isSignItemEditable) {\n                    newSignItems[item.data.id] = {\n                        type_id: item.data.type_id,\n                        required: item.data.required,\n                        name: item.data.name || false,\n                        option_ids: item.data.option_ids,\n                        responsible_id: responsible,\n                        page: page,\n                        posX: item.data.posX,\n                        posY: item.data.posY,\n                        width: item.data.width,\n                        height: item.data.height,\n                    };\n                }\n            }\n        }\n\n        return [signatureValues, frameValues, newSignItems];\n    }\n\n    getSignatureValueFromElement(item) {\n        const types = {\n            text: () => {\n                const textValue =\n                    item.el.textContent && item.el.textContent.trim() ? item.el.textContent : false;\n                const value =\n                    item.el.value && item.el.value.trim()\n                        ? item.el.value\n                        : item.el.querySelector(\"input\")?.value || false;\n                return value || textValue;\n            },\n            initial: () => item.el.dataset.signature,\n            signature: () => item.el.dataset.signature,\n            textarea: () => this.textareaApplyLineBreak(item.el),\n            selection: () => (item.el.value && item.el.value.trim() ? item.el.value : false),\n            checkbox: () => {\n                if (item.el.checked) {\n                    return \"on\";\n                } else {\n                    return item.data.required ? false : \"off\";\n                }\n            },\n            radio: () => {\n                if(item.el.checked) {\n                    return \"on\";\n                } else {\n                    return \"off\";\n                }\n            }\n        };\n        const type = item.data.type;\n        return type in types ? types[type]() : types[\"text\"]();\n    }\n\n    textareaApplyLineBreak(element) {\n        // Removing wrap in order to have scrollWidth > width\n        element.setAttribute(\"wrap\", \"off\");\n\n        const strRawValue = element.value;\n        element.value = \"\";\n\n        const nEmptyWidth = element.scrollWidth;\n        let nLastWrappingIndex = -1;\n\n        // Computing new lines\n        strRawValue.split(\"\").forEach((curChar, i) => {\n            element.value += curChar;\n\n            if (curChar === \" \" || curChar === \"-\" || curChar === \"+\") {\n                nLastWrappingIndex = i;\n            }\n\n            if (element.scrollWidth > nEmptyWidth) {\n                let buffer = \"\";\n                if (nLastWrappingIndex >= 0) {\n                    for (let j = nLastWrappingIndex + 1; j < i; j++) {\n                        buffer += strRawValue.charAt(j);\n                    }\n                    nLastWrappingIndex = -1;\n                }\n                buffer += curChar;\n                element.value = element.value.substr(0, element.value.length - buffer.length);\n                element.value += \"\\n\" + buffer;\n            }\n        });\n        element.setAttribute(\"wrap\", \"\");\n        return element.value;\n    }\n\n    disableItems() {\n        const items = this.root.querySelectorAll(\".o_sign_sign_item\");\n        for (const item of Array.from(items)) {\n            item.classList.add(\"o_sign_sign_item_pdfview\");\n        }\n    }\n\n    openThankYouDialog() {\n        this.dialog.add(ThankYouDialog, {\n            redirectURL: this.props.redirectURL,\n            redirectURLText: this.props.redirectURLText,\n        });\n    }\n\n    async openAuthDialog() {\n        const authDialog = await this.getAuthDialog();\n        if (authDialog.component) {\n            this.closeFn = this.dialog.add(authDialog.component, authDialog.props, {\n                onClose: () => {\n                    this.props.validateButton.removeAttribute(\"disabled\");\n                },\n            });\n        } else {\n            this._sign();\n        }\n    }\n\n    async getAuthDialog() {\n        if (this.props.authMethod === \"sms\" && !this.signatureInfo.smsToken) {\n            const credits = await rpc(\"/sign/has_sms_credits\");\n            if (credits) {\n                return {\n                    component: SMSSignerDialog,\n                    props: {\n                        signerPhone: this.props.signerPhone,\n                        postValidation: (code) => {\n                            this.signatureInfo.smsToken = code;\n                            return this._signDocument();\n                        },\n                    },\n                };\n            }\n            return false;\n        }\n        return false;\n    }\n}\n", "/** @odoo-module **/\n\nimport { setRecurringAnimationFrame, debounce } from \"@web/core/utils/timing\";\nconst MIN_ID = -(2 ** 30);\n\n/**\n * Adds helper lines to the document\n *\n * @param {HTMLElement} target\n * @returns {Object} helpers\n * @returns {Function} helpers.show shows the helper lines at a certain sign item\n * @returns {Function} helpers.hide hides the helper lines\n */\nexport function startHelperLines(target) {\n    function showHelperLinesAt(signItem, coords) {\n        const calculate = {\n            left: (pos) => ({ left: `${pos.left}px` }),\n            right: (pos) => ({ left: `${pos.left + pos.width}px` }),\n            top: (pos) => ({ top: `${pos.top}px` }),\n            bottom: (pos) => ({ top: `${pos.top + pos.height}px` }),\n        };\n\n        const rect = signItem.getBoundingClientRect();\n        const positions = {\n            top: (coords && coords.y) || rect.top,\n            left: (coords && coords.x) || rect.left,\n            height: signItem.clientHeight,\n            width: signItem.clientWidth,\n        };\n        for (const line in helperLines) {\n            const newPos = calculate[line](positions);\n            Object.assign(helperLines[line].style, {\n                visibility: \"visible\",\n                ...newPos,\n            });\n        }\n    }\n\n    function hideHelperLines() {\n        for (const line in helperLines) {\n            helperLines[line].style.visibility = \"hidden\";\n        }\n    }\n\n    const top = target.createElement(\"div\");\n    const bottom = target.createElement(\"div\");\n    top.className = \"o_sign_drag_helper o_sign_drag_top_helper\";\n    bottom.className = \"o_sign_drag_helper o_sign_drag_top_helper\";\n    const left = target.createElement(\"div\");\n    const right = target.createElement(\"div\");\n    left.className = \"o_sign_drag_helper o_sign_drag_side_helper\";\n    right.className = \"o_sign_drag_helper o_sign_drag_side_helper\";\n\n    const body = target.querySelector(\"body\");\n    body.appendChild(top);\n    body.appendChild(bottom);\n    body.appendChild(left);\n    body.appendChild(right);\n\n    const helperLines = {\n        top,\n        bottom,\n        left,\n        right,\n    };\n\n    return {\n        show: showHelperLinesAt,\n        hide: hideHelperLines,\n    };\n}\n\nexport function isVisible(e) {\n    return e && !!(e.offsetWidth || e.offsetHeight || e.getClientRects().length);\n}\n\nexport function offset(el) {\n    const box = el.getBoundingClientRect();\n    const docElem = document.documentElement;\n    return {\n        top: box.top + window.scrollY - docElem.clientTop,\n        left: box.left + window.scrollY - docElem.clientLeft,\n    };\n}\n\n/**\n * Normalizes the normalize position of a sign item to prevent dropping outside the page\n * @param {Number} position x/y position\n * @param {Number} itemDimension size of item at x/y direction\n * @returns {Number}\n */\nexport function normalizePosition(position, itemDimension) {\n    if (position < 0) {\n        return 0;\n    } else if (position + itemDimension > 1.0) {\n        return 1.0 - itemDimension;\n    }\n    return position;\n}\n\n/**\n * Normalizes the new dimension of a sign item to prevent it from resizing outside the page\n * @param {Number} dimension\n * @param {Number} position\n * @returns {Number} normalized dimension\n */\nexport function normalizeDimension(dimension, position) {\n    if (position + dimension > 1) {\n        return 1 - position;\n    }\n    return dimension;\n}\n\n/**\n * Generates a random negative ID to be added to sign items that were just created and are not in the DB yet\n * @returns {Number}\n */\nexport function generateRandomId() {\n    return Math.floor(Math.random() * MIN_ID) - 1;\n}\n\n/**\n * Adds smooth scrolling while dragging elements\n * @param {HTMLElement} container the container that sets the reference for scrolling\n * @param {HTMLElement} element the element being dragged\n * @param {HTMLElement || null} dragImageElement in some cases the element being dragged is not the same size as the dragImageElement\n * @param {HelperLines} helperLines instance of helper lines for guiding the user while dragging\n * @returns {Function} cleanup function to be executed when dragging is over\n */\nexport function startSmoothScroll(container, element, dragImageElement = null, helperLines) {\n    const boundary = 0.2;\n    const directions = {\n        up: -1,\n        down: 1,\n        left: -1,\n        right: 1,\n    };\n    const mouse = {};\n    const containerOffset = offset(container);\n    const dragAmount = 10;\n    const el = dragImageElement || element;\n    function updateMousePosition(e) {\n        // calculates the event's position relative to the container\n        mouse.x = e.clientX - containerOffset.left;\n        mouse.y = e.clientY - containerOffset.top;\n        helperLines.show(el, { x: e.clientX, y: e.clientY });\n    }\n    const debouncedOnMouseMove = debounce(updateMousePosition, \"animationFrame\", true);\n    container.addEventListener(\"dragover\", debouncedOnMouseMove);\n    const cleanup = setRecurringAnimationFrame(() => {\n        const { x, y } = mouse;\n        let scrollX,\n            scrollY = 0;\n        if (x <= container.clientWidth * boundary) {\n            scrollX = directions.left * dragAmount;\n        } else if (x >= container.clientWidth * (1 - boundary)) {\n            scrollX = directions.right * dragAmount;\n        }\n\n        if (y <= container.clientHeight * boundary) {\n            scrollY = directions.up * dragAmount;\n        } else if (y >= container.clientHeight * (1 - boundary)) {\n            scrollY = directions.down * dragAmount;\n        }\n        container.scrollBy(scrollX, scrollY);\n    });\n    return () => {\n        cleanup();\n        container.removeEventListener(\"dragover\", debouncedOnMouseMove);\n        helperLines.hide();\n    };\n}\n\n/**\n * Adds resizing functionality to a sign item\n * @param {SignItem} signItem\n * @param {Function} onResize\n */\nexport function startResize(signItem, onResize) {\n    const page = signItem.el.parentElement;\n    const mouse = {};\n    const resizeHandleWidth = signItem.el.querySelector(\".resize_width\");\n    const resizeHandleHeight = signItem.el.querySelector(\".resize_height\");\n    const resizeHandleBoth = signItem.el.querySelector(\".resize_both\");\n\n    const computeDimensions = (e) => {\n        const { direction, x, y } = mouse;\n        const computedStyle = getComputedStyle(signItem.el);\n        const signItemAbsoluteWidth = parseInt(computedStyle.width);\n        const signItemAbsoluteHeight = parseInt(computedStyle.height);\n        const dX = e.clientX - x;\n        const dY = e.clientY - y;\n\n        Object.assign(mouse, {\n            x: e.clientX,\n            y: e.clientY,\n        });\n\n        const factor = {\n            x: (dX + signItemAbsoluteWidth) / signItemAbsoluteWidth,\n            y: (dY + signItemAbsoluteHeight) / signItemAbsoluteHeight,\n        };\n\n        if (dX < 0 && Math.abs(dX) >= signItemAbsoluteWidth) {\n            factor.x = 1;\n        }\n\n        if (dY < 0 && Math.abs(dY) >= signItemAbsoluteHeight) {\n            factor.y = 1;\n        }\n\n        const width =\n            direction === \"width\" || direction === \"both\"\n                ? Math.round(\n                      normalizeDimension(factor.x * signItem.data.width, signItem.data.posX) * 1000\n                  ) / 1000\n                : signItem.data.width;\n\n        const height =\n            direction === \"height\" || direction === \"both\"\n                ? Math.round(\n                      normalizeDimension(factor.y * signItem.data.height, signItem.data.posY) * 1000\n                  ) / 1000\n                : signItem.data.height;\n\n        return { height, width };\n    };\n\n    const handleMouseMove = (e) => {\n        if (signItem.el.classList.contains(\"o_resizing\")) {\n            e.preventDefault();\n            onResize(signItem, computeDimensions(e), false);\n        }\n    };\n\n    const debouncedOnMouseMove = debounce(handleMouseMove, \"animationFrame\", true);\n    const handleMouseDown = (e, direction) => {\n        e.preventDefault();\n        signItem.el.classList.add(\"o_resizing\");\n        Object.assign(mouse, { x: e.clientX, y: e.clientY, direction });\n        page.addEventListener(\"mousemove\", debouncedOnMouseMove);\n    };\n\n    resizeHandleWidth.addEventListener(\"mousedown\", (e) => handleMouseDown(e, \"width\"));\n    resizeHandleHeight.addEventListener(\"mousedown\", (e) => handleMouseDown(e, \"height\"));\n    resizeHandleBoth.addEventListener(\"mousedown\", (e) => handleMouseDown(e, \"both\"));\n\n    page.addEventListener(\"mouseup\", (e) => {\n        if (signItem.el.classList.contains(\"o_resizing\")) {\n            signItem.el.classList.remove(\"o_resizing\");\n            page.removeEventListener(\"mousemove\", debouncedOnMouseMove);\n            onResize(signItem, computeDimensions(e), true);\n        }\n    });\n}\n\n/**\n * Adds pinch listeners to zoom in/zoom out of iframe when in mobile\n * @param {HTMLElement} target\n * @param {handlers} handlers\n * @param {Function} handlers.increaseDistanceHandler Handler called when the distance pinched between the 2 pointer is decreased\n * @param {Function} handlers.decreaseDistanceHandler Handler called when the distance pinched between the 2 pointer is increased\n */\nexport function pinchService(target, handlers) {\n    let prevDiff = null;\n    const { increaseDistanceHandler, decreaseDistanceHandler } = handlers;\n\n    target.addEventListener(\"touchstart\", reset);\n    target.addEventListener(\"touchmove\", touchMove);\n    target.addEventListener(\"touchend\", reset);\n\n    /**\n     * This function implements a 2-pointer horizontal pinch/zoom gesture.\n     *\n     * If the distance between the two pointers has increased (zoom in),\n     * distance is decreasing (zoom out)\n     *\n     * @param e\n     * @private\n     */\n    function touchMove(e) {\n        const touches = e.touches;\n        // If two pointers are down, check for pinch gestures\n        if (touches.length === 2) {\n            // Calculate the current distance between the 2 fingers\n            const deltaX = touches[0].pageX - touches[1].pageX;\n            const deltaY = touches[0].pageY - touches[1].pageY;\n            const curDiff = Math.hypot(deltaX, deltaY);\n            if (prevDiff === null) {\n                prevDiff = curDiff;\n            }\n            const scale = prevDiff / curDiff;\n            if (scale < 1) {\n                decreaseDistanceHandler(e);\n            } else if (scale > 1) {\n                increaseDistanceHandler(e);\n            }\n        }\n    }\n\n    function reset() {\n        prevDiff = null;\n    }\n\n    return () => {\n        target.removeEventListener(\"touchstart\", reset);\n        target.removeEventListener(\"touchmove\", touchMove);\n        target.removeEventListener(\"touchend\", reset);\n    };\n}\n\n/**\n * Generates the PDF.JS URL from the attachment location\n * @param { String } attachmentLocation\n * @param { Boolean } isSmall\n * @returns\n */\nexport function buildPDFViewerURL(attachmentLocation, isSmall) {\n    const date = new Date().toISOString();\n    const baseURL = \"/web/static/lib/pdfjs/web/viewer.html\";\n    // encodes single quote and double quotes as encodeURIComponent does not handle those\n    attachmentLocation = encodeURIComponent(attachmentLocation)\n        .replace(/'/g, \"%27\")\n        .replace(/\"/g, \"%22\");\n    const zoom = isSmall ? \"page-fit\" : \"page-width\";\n    return `${baseURL}?unique=${date}&file=${attachmentLocation}#page=1&zoom=${zoom}`;\n}\n", "/** @odoo-module **/\n\nimport { InitialsAllPagesDialog } from \"./initials_all_pages_dialog\";\nimport { PublicSignerDialog } from \"./public_signer_dialog\";\nimport { SignNameAndSignatureDialog } from \"./sign_name_and_signature_dialog\";\nimport { SMSSignerDialog } from \"./sms_signer_dialog\";\nimport { ThankYouDialog } from \"./thank_you_dialog\";\nimport { NextDirectSignDialog } from \"./next_direct_sign_dialog\";\nimport { EncryptedDialog } from \"./encrypted_dialog\";\nimport { SignRefusalDialog } from \"./sign_refusal_dialog\";\n\nexport {\n    InitialsAllPagesDialog,\n    PublicSignerDialog,\n    SignNameAndSignatureDialog,\n    SMSSignerDialog,\n    ThankYouDialog,\n    NextDirectSignDialog,\n    EncryptedDialog,\n    SignRefusalDialog,\n};\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { Component, useRef } from \"@odoo/owl\";\n\nexport class EncryptedDialog extends Component {\n    static template = \"sign.EncryptedDialog\";\n    static components = {\n        Dialog,\n    };\n    static props = {\n        close: Function,\n    };\n\n    setup() {\n        this.passwordInput = useRef(\"password\");\n        this.dialog = useService(\"dialog\");\n        this.signInfo = useService(\"signInfo\");\n    }\n\n    get dialogProps() {\n        return {\n            title: _t(\"PDF is encrypted\"),\n            fullscreen: this.env.isSmall,\n            size: \"md\",\n        };\n    }\n\n    async validatePassword() {\n        const passwordInput = this.passwordInput.el;\n        if (!passwordInput.value) {\n            passwordInput.classList.toggle(\"is-invalid\", !passwordInput.value);\n            return false;\n        }\n\n        const route = `/sign/password/${this.signInfo.get(\"documentId\")}`;\n        const params = {\n            password: passwordInput.value,\n        };\n\n        const response = await rpc(route, params);\n        if (!response) {\n            return this.dialog.add(AlertDialog, {\n                body: _t(\"Password is incorrect.\"),\n            });\n        } else {\n            this.props.close();\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component, useRef } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nexport class InitialsAllPagesDialog extends Component {\n    static template = \"sign.InitialsAllPagesDialog\";\n    static components = {\n        Dialog,\n    };\n    static props = {\n        addInitial: Function,\n        close: Function,\n        roles: Object,\n        responsible: Number,\n        pageCount: Number,\n    };\n\n    setup() {\n        this.selectRef = useRef(\"role_select\");\n    }\n\n    get currentRole() {\n        return parseInt(this.selectRef.el?.value);\n    }\n\n    onAddOnceClick() {\n        this.props.addInitial(this.currentRole, false);\n        this.props.close();\n    }\n\n    onAddToAllPagesClick() {\n        this.props.addInitial(this.currentRole, true);\n        this.props.close();\n    }\n\n    get dialogProps() {\n        return {\n            size: \"md\",\n            title: _t(\"Add Initials\"),\n        };\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nexport class NextDirectSignDialog extends Component {\n    static template = \"sign.NextDirectSignDialog\";\n    static components = {\n        Dialog,\n    };\n    static props = {\n        close: Function,\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.signInfo = useService(\"signInfo\");\n        this.title = _t(\"Thank You!\");\n    }\n\n    goToNextSigner() {\n        const newCurrentToken = this.signInfo.get(\"tokenList\").shift();\n        this.signInfo.get(\"nameList\").shift();\n        this.action.doAction(\n            {\n                type: \"ir.actions.client\",\n                tag: \"sign.SignableDocument\",\n                name: _t(\"Sign\"),\n            },\n            {\n                additionalContext: {\n                    id: this.signInfo.get(\"documentId\"),\n                    create_uid: this.signInfo.get(\"createUid\"),\n                    state: this.signInfo.get(\"signRequestState\"),\n                    token: newCurrentToken,\n                    token_list: this.signInfo.get(\"tokenList\"),\n                    name_list: this.signInfo.get(\"nameList\"),\n                },\n                stackPosition: \"replaceCurrentAction\",\n            }\n        );\n        this.props.close();\n    }\n\n    get nextSigner() {\n        return this.signInfo.get(\"nameList\")[0];\n    }\n\n    get dialogProps() {\n        return {\n            size: \"md\",\n            technical: this.env.isSmall,\n            fullscreen: this.env.isSmall,\n        };\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, useRef } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nexport class PublicSignerDialog extends Component {\n    static template = \"sign.PublicSignerDialog\";\n    static components = {\n        Dialog,\n    };\n    static props = {\n        name: String,\n        mail: String,\n        postValidation: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.nameInput = useRef(\"name\");\n        this.mailInput = useRef(\"mail\");\n        this.signInfo = useService(\"signInfo\");\n    }\n\n    get dialogProps() {\n        return {\n            title: _t(\"Final Validation\"),\n            size: \"md\",\n            technical: this.env.isSmall,\n            fullscreen: this.env.isSmall,\n        };\n    }\n\n    async submit() {\n        const name = this.nameInput.el.value;\n        const mail = this.mailInput.el.value;\n        if (!this.validateForm(name, mail)) {\n            return false;\n        }\n\n        const response = await rpc(\n            `/sign/send_public/${this.signInfo.get(\"documentId\")}/${this.signInfo.get(\n                \"signRequestToken\"\n            )}`,\n            { name, mail }\n        );\n\n        await this.props.postValidation(\n            response[\"requestID\"],\n            response[\"requestToken\"],\n            response[\"accessToken\"]\n        );\n        this.props.close();\n    }\n\n    validateForm(name, mail) {\n        const isEmailInvalid = !mail || mail.indexOf(\"@\") < 0;\n        if (!name || isEmailInvalid) {\n            this.nameInput.el.classList.toggle(\"is-invalid\", !name);\n            this.mailInput.el.classList.toggle(\"is-invalid\", isEmailInvalid);\n            return false;\n        }\n        return true;\n    }\n}\n", "/** @odoo-module **/\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { _t } from \"@web/core/l10n/translation\";\n/* global html2canvas */\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { user } from \"@web/core/user\";\nimport { loadJS } from \"@web/core/assets\";\nimport { Component, onWillStart, useRef, useState } from \"@odoo/owl\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { NameAndSignature } from \"@web/core/signature/name_and_signature\";\n\nexport class SignNameAndSignature extends NameAndSignature {\n    static template = \"sign.NameAndSignature\";\n    static props = {\n        ...NameAndSignature.props,\n        activeFrame: Boolean,\n        defaultFrame: String,\n        frame: { type: Object, optional: true },\n        hash: String,\n        onNameChange: Function,\n    };\n\n    setup() {\n        super.setup();\n        this.props.signature.signatureChanged = this.state.signMode !== \"draw\";\n\n        if (this.props.frame) {\n            this.state.activeFrame = this.props.activeFrame || false;\n            this.frame = this.props.defaultFrame;\n\n            this.signFrame = useRef(\"signFrame\");\n            this.props.frame.updateFrame = () => {\n                if (this.state.activeFrame) {\n                    this.props.signature.signatureChanged = true;\n                    const xOffset = localization.direction === \"rtl\" ? 0.75 : 0.06; // magic numbers\n                    this.signFrame.el.classList.toggle(\"active\", true);\n                    return html2canvas(this.signFrame.el, {\n                        backgroundColor: null,\n                        width: this.signatureRef.el.width,\n                        height: this.signatureRef.el.height,\n                        x: -this.signatureRef.el.width * xOffset,\n                        y: -this.signatureRef.el.height * 0.09,\n                    }).then((canvas) => {\n                        this.frame = canvas.toDataURL(\"image/png\");\n                    });\n                }\n                return Promise.resolve(false);\n            };\n\n            this.props.frame.getFrameImageSrc = () => {\n                return this.state.activeFrame ? this.frame : false;\n            };\n        }\n\n        onWillStart(() => {\n            if (this.props.frame) {\n                return Promise.all([\n                    user.hasGroup(\"base.group_user\").then((isSystemUser) => {\n                        this.showFrameCheck = isSystemUser;\n                    }),\n                    loadJS(\"/web_editor/static/lib/html2canvas.js\"),\n                ]);\n            }\n        });\n    }\n\n    onFrameChange() {\n        this.state.activeFrame = !this.state.activeFrame;\n    }\n\n    onSignatureAreaClick() {\n        if (this.state.signMode === \"draw\") {\n            this.props.signature.signatureChanged = true;\n        }\n    }\n\n    onClickSignLoad() {\n        super.onClickSignLoad();\n        this.props.signature.signatureChanged = true;\n    }\n\n    async onClickSignAuto() {\n        super.onClickSignAuto();\n        this.props.signature.signatureChanged = true;\n        if (this.fonts.length <= 1) {\n            this.fonts = await rpc(`/web/sign/get_fonts/`);\n        }\n    }\n\n    onClickSignDrawClear() {\n        super.onClickSignDrawClear();\n        this.props.signature.signatureChanged = true;\n    }\n\n    get signFrameClass() {\n        return this.state.activeFrame && this.state.signMode !== \"draw\" ? \"active\" : \"\";\n    }\n\n    /**\n     * Override to enable/disable SignNameAndSignatureDialog's footer buttons\n     * @param { Event } e\n     */\n    onInputSignName(e) {\n        super.onInputSignName(e);\n        this.props.onNameChange(this.props.signature.name);\n    }\n}\n\nexport class SignNameAndSignatureDialog extends Component {\n    static props = {\n        signature: Object,\n        frame: { type: Object, optional: true },\n        signatureType: { type: String, optional: true },\n        displaySignatureRatio: Number,\n        activeFrame: Boolean,\n        defaultFrame: { type: String, optional: true },\n        mode: { type: String, optional: true },\n        signatureImage: { type: String, optional: true },\n        hash: String,\n        onConfirm: Function,\n        onConfirmAll: Function,\n        onCancel: Function,\n        close: Function,\n    };\n    static template = \"sign.SignNameAndSignatureDialog\";\n    static components = {\n        Dialog,\n        SignNameAndSignature,\n    };\n\n    setup() {\n        this.footerState = useState({\n            buttonsDisabled: !this.props.signature.name,\n        });\n    }\n\n    get nameAndSignatureProps() {\n        return {\n            signature: this.props.signature || \"signature\",\n            signatureType: this.props.signatureType,\n            displaySignatureRatio: this.props.displaySignatureRatio,\n            activeFrame: this.props.activeFrame,\n            defaultFrame: this.props.defaultFrame || \"\",\n            mode: this.props.mode || \"auto\",\n            frame: this.props.frame || false,\n            hash: this.props.hash,\n            onNameChange: this.onNameChange.bind(this),\n            defaultFont: \"LaBelleAurore-Regular.ttf\",\n        };\n    }\n\n    get dialogProps() {\n        return {\n            title: _t(\"Adopt Your Signature\"),\n            size: \"md\",\n        };\n    }\n\n    onNameChange(name) {\n        if (this.footerState.buttonsDisabled !== !name) {\n            this.footerState.buttonsDisabled = !name;\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, useRef } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { ThankYouDialog } from \"./thank_you_dialog\";\n\nexport class SignRefusalDialog extends Component {\n    static template = \"sign.SignRefusalDialog\";\n    static components = {\n        Dialog,\n    };\n    static props = {\n        close: Function,\n    };\n\n    setup() {\n        this.refuseReasonEl = useRef(\"refuse-reason\");\n        this.refuseButton = useRef(\"refuse-button\");\n        this.dialog = useService(\"dialog\");\n        this.signInfo = useService(\"signInfo\");\n    }\n\n    get dialogProps() {\n        return {\n            size: \"md\",\n            title: _t(\"Refuse to sign\"),\n        };\n    }\n\n    checkForChanges() {\n        const value = this.refuseReasonEl.el.value.trim();\n        this.refuseButton.el.disabled = value.length === 0 ? \"disabled\" : \"\";\n    }\n\n    async refuse() {\n        const reason = this.refuseReasonEl.el.value;\n        const route = `/sign/refuse/${this.signInfo.get(\"documentId\")}/${this.signInfo.get(\n            \"signRequestItemToken\"\n        )}`;\n        const params = {\n            refusal_reason: reason,\n        };\n        const response = await rpc(route, params);\n        if (!response) {\n            this.dialog.add(\n                AlertDialog,\n                {\n                    body: _t(\"Sorry, you cannot refuse this document\"),\n                },\n                {\n                    onClose: () => window.location.reload(),\n                }\n            );\n        }\n        this.dialog.add(ThankYouDialog, {\n            subtitle: _t(\"The document has been refused\"),\n            message: _t(\n                \"We'll send an email to warn other contacts in copy & signers with the reason you provided.\"\n            ),\n        });\n\n        this.props.close();\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component, useState, useRef, useEffect } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport class SMSSignerDialog extends Component {\n    static template = \"sign.SMSSignerDialog\";\n    static components = {\n        Dialog,\n    };\n    static props = {\n        signerPhone: {\n            type: String,\n            optional: true,\n        },\n        postValidation: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.validationCodeInput = useRef(\"code\");\n        this.phoneInput = useRef(\"phone\");\n        this.dialog = useService(\"dialog\");\n        this.signInfo = useService(\"signInfo\");\n        this.SMSInfo = { phoneNumber: this.props.signerPhone || \"\" };\n        this.state = useState({\n            sendingSMS: false,\n            SMSCount: 0,\n        });\n\n        useEffect(\n            () => {\n                return () => {\n                    browser.clearTimeout(this.timeout);\n                };\n            },\n            () => []\n        );\n    }\n\n    sendSMS(phoneNumber) {\n        this.state.sendingSMS = true;\n        const route = `/sign/send-sms/${this.signInfo.get(\"documentId\")}/${this.signInfo.get(\n            \"signRequestItemToken\"\n        )}/${phoneNumber}`;\n        rpc(route)\n            .then((success) => {\n                if (success) {\n                    this.handleSendSMSSuccess();\n                } else {\n                    this.handleSMSError();\n                }\n            })\n            .catch((_) => {\n                this.handleSMSError();\n            });\n    }\n\n    handleSendSMSSuccess() {\n        this.timeout = browser.setTimeout(() => {\n            this.state.sendingSMS = false;\n            this.state.SMSCount++;\n        }, 15000);\n    }\n\n    handleSMSError() {\n        this.state.sendingSMS = false;\n        this.dialog.add(AlertDialog, {\n            title: _t(\"Error\"),\n            body: _t(\"Unable to send the SMS, please contact the sender of the document.\"),\n        });\n    }\n\n    onSendSMSClick(e) {\n        const sendButton = e.target;\n        sendButton.setAttribute(\"disabled\", true);\n        const phoneNumber = this.phoneInput.el.value;\n        if (phoneNumber) {\n            this.SMSInfo.phoneNumber = phoneNumber;\n            this.sendSMS(phoneNumber);\n        }\n        sendButton.removeAttribute(\"disabled\");\n    }\n\n    async validateSMS(e) {\n        const validateButton = e.target;\n        const validationCode = this.validationCodeInput.el?.value;\n        if (!validationCode) {\n            this.validationCodeInput.el.classList.toggle(\"is-invalid\");\n            return false;\n        }\n        validateButton.setAttribute(\"disabled\", true);\n        await this.props.postValidation(validationCode);\n        validateButton.removeAttribute(\"disabled\");\n        this.props.close();\n    }\n\n    get dialogProps() {\n        return {\n            size: \"md\",\n            title: _t(\"Final Validation\"),\n            fullscreen: this.env.isSmall,\n        };\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { session } from \"@web/session\";\nimport { user } from \"@web/core/user\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { EncryptedDialog } from \"./encrypted_dialog\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\n\nexport class ThankYouDialog extends Component {\n    static template = \"sign.ThankYouDialog\";\n    static components = {\n        Dialog,\n    };\n    static props = {\n        message: {\n            type: String,\n            optional: true,\n        },\n        subtitle: {\n            type: String,\n            optional: true,\n        },\n        redirectURL: {\n            type: String,\n            optional: true,\n        },\n        redirectURLText: {\n            type: String,\n            optional: true,\n        },\n        close: Function,\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.signInfo = useService(\"signInfo\");\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            nextDocuments: [],\n            buttons: [],\n        });\n        this.redirectURL = this.processURL(this.props.redirectURL);\n        this.message =\n            this.props.message || _t(\"You will receive the final signed document by email.\");\n        onWillStart(this.willStart);\n    }\n\n    get suggestSignUp() {\n        return user.userId === false;\n    }\n\n    get dialogProps() {\n        return {\n            size: \"md\",\n        };\n    }\n\n    async checkIfEncryptedDialog() {\n        const route = `/sign/encrypted/${this.signInfo.get(\"documentId\")}`;\n        return rpc(route);\n    }\n\n    async willStart() {\n        const isEncrypted = await this.checkIfEncryptedDialog();\n        if (isEncrypted) {\n            this.dialog.add(EncryptedDialog);\n        }\n        this.signRequestState = await rpc(\n            `/sign/sign_request_state/${this.signInfo.get(\"documentId\")}/${this.signInfo.get(\n                \"signRequestToken\"\n            )}`\n        );\n        this.closeLabel = _t(\"Close\");\n        if (!session.is_frontend) {\n            const result = await this.orm.call(\"sign.request\", \"get_close_values\", [\n                [this.signInfo.get(\"documentId\")],\n            ]);\n            this.closeAction = result.action;\n            this.closeLabel = result.label;\n            const closeContext = result.custom_action ? {} : { clearBreadcrumbs: true };\n            this.closeContext = closeContext;\n        }\n        if (!this.suggestSignUp && !session.is_website_user) {\n            const result = await rpc(\"/sign/sign_request_items\", {\n                request_id: this.signInfo.get(\"documentId\"),\n                token: this.signInfo.get(\"signRequestToken\"),\n            });\n            if (result && result.length) {\n                this.state.nextDocuments = result.map((doc) => {\n                    return {\n                        id: doc.id,\n                        name: doc.name,\n                        date: doc.date,\n                        user: doc.user,\n                        accessToken: doc.token,\n                        requestId: doc.requestId,\n                        canceled: false,\n                    };\n                });\n            }\n        }\n\n        this.generateButtons();\n    }\n\n    generateButtons() {\n        if (this.redirectURL) {\n            this.state.buttons.push({\n                name: this.props.redirectURLText,\n                click: () => {\n                    window.location.assign(this.redirectURL);\n                },\n            });\n        }\n\n        if (this.signRequestState === \"signed\") {\n            this.state.buttons.push({\n                name: _t(\"Download Document\"),\n                click: this.downloadDocument,\n            });\n        }\n\n        if (this.suggestSignUp) {\n            this.message += _t(\" You can safely close this window.\");\n            this.state.buttons.push({\n                name: _t(\"Sign Up for free\"),\n                classes: \"btn btn-link ms-auto\",\n                ignored: true,\n                click: () => {\n                    window.open(\n                        \"https://www.odoo.com/trial?selected_app=sign&utm_source=db&utm_medium=sign\",\n                        \"_blank\"\n                    );\n                },\n            });\n        } else {\n            this.state.buttons.push({\n                name: this.closeLabel,\n                click: () => {\n                    if (session.is_frontend) {\n                        const signatureRequestId = this.signInfo.get(\"documentId\");\n                        window.location.assign(`/my/signature/${signatureRequestId}`);\n                    } else {\n                        this.props.close();\n                        this.env.services.action.doAction(this.closeAction, this.closeContext);\n                    }\n                },\n            });\n        }\n\n        for (let i = 0; i < this.state.buttons.length; i++) {\n            if (this.state.buttons[i].ignored) {\n                continue;\n            }\n            const buttonClass = i === 0 ? \"btn btn-primary\" : \"btn btn-secondary\";\n            this.state.buttons[i].classes = `${this.state.buttons[i].classes} ${buttonClass}`;\n        }\n    }\n\n    processURL(url) {\n        if (url && !/^(f|ht)tps?:\\/\\//i.test(url)) {\n            url = `http://${url}`;\n        }\n        return url;\n    }\n\n    goToDocument(id, token) {\n        window.location.assign(this.makeURI(\"/sign/document\", id, token, undefined, { portal: 1 }));\n    }\n\n    clickNextSign(id, token) {\n        this.goToDocument(id, token);\n    }\n\n    clickButtonNext() {\n        const nextDocument = this.state.nextDocuments.find((document) => !document.canceled);\n        this.goToDocument(nextDocument.requestId, nextDocument.accessToken);\n    }\n\n    async clickNextCancel(doc) {\n        await this.orm.call(\"sign.request\", \"cancel\", [doc.requestID]);\n        this.state.nextDocuments = this.state.nextDocuments.map((nextDoc) => {\n            if (nextDoc.id === doc.id) {\n                return {\n                    ...nextDoc,\n                    canceled: true,\n                };\n            }\n            return nextDoc;\n        });\n        if (this.state.nextDocuments.every((doc) => doc.canceled)) {\n            this.state.buttons = this.state.buttons.map((button) => {\n                if (button.name === _t(\"Sign Next Document\")) {\n                    return {\n                        ...button,\n                        disabled: true,\n                    };\n                }\n                return button;\n            });\n        }\n    }\n\n    async downloadDocument() {\n        // Simply triggers a download of the document which the user just signed.\n        window.location.assign(\n            this.makeURI(\n                \"/sign/download\",\n                this.signInfo.get(\"documentId\"),\n                this.signInfo.get(\"signRequestToken\"),\n                \"/completed\"\n            )\n        );\n    }\n\n    makeURI(baseUrl, requestID, token, suffix = \"\", params = \"\") {\n        // Helper function for constructing a URI.\n        params = params ? \"?\" + new URLSearchParams(params).toString() : \"\";\n        return `${baseUrl}/${requestID}/${token}${suffix}${params}`;\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\n\nexport const signInfoService = {\n    dependencies: [],\n    start() {\n        let signInfo = {};\n\n        function set(data) {\n            Object.assign(signInfo, data);\n        }\n\n        function reset(data) {\n            signInfo = data;\n        }\n\n        function get(key) {\n            return signInfo[key];\n        }\n\n        return {\n            set,\n            reset,\n            get,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"signInfo\", signInfoService);\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { loadWysiwygFromTextarea } from \"@web_editor/js/frontend/loadWysiwygFromTextarea\";\nimport { redirect } from \"@web/core/utils/urls\";\n\npublicWidget.registry.websiteProfile = publicWidget.Widget.extend({\n    selector: '.o_wprofile_email_validation_container',\n    read_events: {\n        'click .send_validation_email': '_onSendValidationEmailClick',\n        'click .validated_email_close': '_onCloseValidatedEmailClick',\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onSendValidationEmailClick: function (ev) {\n        ev.preventDefault();\n        const element = ev.currentTarget;\n        rpc('/profile/send_validation_email', {\n            redirect_url: element.dataset[\"redirect_url\"],\n        }).then(function (data) {\n            if (data) {\n                redirect(element.dataset[\"redirect_url\"]);\n            }\n        });\n    },\n\n    /**\n     * @private\n     */\n    _onCloseValidatedEmailClick: function () {\n        rpc('/profile/validate_email/close');\n    },\n});\n\npublicWidget.registry.websiteProfileEditor = publicWidget.Widget.extend({\n    selector: '.o_wprofile_editor_form',\n    read_events: {\n        'click .o_forum_profile_pic_edit': '_onEditProfilePicClick',\n        'change .o_forum_file_upload': '_onFileUploadChange',\n        'click .o_forum_profile_pic_clear': '_onProfilePicClearClick',\n        'click .o_forum_profile_bio_edit': '_onProfileBioEditClick',\n        'click .o_forum_profile_bio_cancel_edit': '_onProfileBioCancelEditClick',\n    },\n\n    /**\n     * @override\n     */\n    start: async function () {\n        const def = this._super.apply(this, arguments);\n        if (this.editableMode) {\n            return def;\n        }\n\n        const textareaEl = this.el.querySelector(\"textarea.o_wysiwyg_loader\");\n\n        const options = {\n            recordInfo: {\n                context: this._getContext(),\n                res_model: \"res.users\",\n                res_id: parseInt(this.el.querySelector(\"input[name=user_id]\").value),\n            },\n            value: textareaEl.getAttribute(\"content\"),\n            resizable: true,\n            userGeneratedContent: true,\n        };\n\n        if (textareaEl.attributes.placeholder) {\n            options.placeholder = textareaEl.attributes.placeholder.value;\n        }\n\n        this._wysiwyg = await loadWysiwygFromTextarea(this, textareaEl, options);\n\n        return Promise.all([def]);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onEditProfilePicClick: function (ev) {\n        ev.preventDefault();\n        ev.currentTarget.closest(\"form\").querySelector(\".o_forum_file_upload\").click();\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onFileUploadChange: function (ev) {\n        if (!ev.currentTarget.files.length) {\n            return;\n        }\n        const formEl = ev.currentTarget.closest(\"form\");\n        var reader = new window.FileReader();\n        reader.readAsDataURL(ev.currentTarget.files[0]);\n        reader.onload = function (ev) {\n            formEl.querySelector(\".o_wforum_avatar_img\").src = ev.target.result;\n        };\n        formEl.querySelector(\"#forum_clear_image\")?.remove();\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onProfilePicClearClick: function (ev) {\n        const formEl = ev.currentTarget.closest(\"form\");\n        formEl.querySelector(\".o_wforum_avatar_img\").src = \"/web/static/img/placeholder.png\";\n        const inputElement = document.createElement(\"input\");\n        inputElement.setAttribute(\"name\", \"clear_image\");\n        inputElement.setAttribute(\"id\", \"forum_clear_image\");\n        inputElement.setAttribute(\"type\", \"hidden\");\n        formEl.append(inputElement);\n    },\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onProfileBioEditClick: function (ev) {\n        ev.preventDefault();\n        ev.currentTarget.classList.add(\"d-none\");\n        document.querySelector(\".o_forum_profile_bio_cancel_edit\").classList.remove(\"d-none\");\n        document.querySelector(\".o_forum_profile_bio\").classList.add(\"d-none\");\n        document.querySelector(\".o_forum_profile_bio_form\").classList.remove(\"d-none\");\n    },\n\n     /**\n     * @private\n     * @param {Event} ev\n     */\n     _onProfileBioCancelEditClick: function (ev) {\n        ev.preventDefault();\n        ev.currentTarget.classList.add(\"d-none\");\n        document.querySelector(\".o_forum_profile_bio_edit\").classList.remove(\"d-none\");\n        document.querySelector(\".o_forum_profile_bio_form\").classList.add(\"d-none\");\n        document.querySelector(\".o_forum_profile_bio\").classList.remove(\"d-none\");\n     },\n});\n\npublicWidget.registry.websiteProfileNextRankCard = publicWidget.Widget.extend({\n    selector: '.o_wprofile_progress_circle',\n\n    /**\n     * @override\n     */\n    start: function () {\n        new Tooltip(this.el.querySelector('g[data-bs-toggle=\"tooltip\"]'));\n        return this._super.apply(this, arguments);\n    },\n\n});\n\nexport default publicWidget.registry.websiteProfile;\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { deserializeDateTime } from \"@web/core/l10n/dates\";\n\npublicWidget.registry.websiteSlides = publicWidget.Widget.extend({\n    selector: '#wrapwrap',\n\n    /**\n     * @override\n     * @param {Object} parent\n     */\n    start: function (parent) {\n        var defs = [this._super.apply(this, arguments)];\n\n        $(\"timeago.timeago\").toArray().forEach((el) => {\n            var datetime = $(el).attr('datetime');\n            var datetimeObj = deserializeDateTime(datetime);\n            // if presentation 7 days, 24 hours, 60 min, 60 second, 1000 millis old(one week)\n            // then return fix formate string else timeago\n            var displayStr = '';\n            if (datetimeObj && new Date().getTime() - datetimeObj.valueOf() > 7 * 24 * 60 * 60 * 1000) {\n                displayStr = datetimeObj.toFormat('DD');\n            } else {\n                displayStr = datetimeObj.toRelative();\n            }\n            $(el).text(displayStr);\n        });\n\n        return Promise.all(defs);\n    },\n});\n\nexport default publicWidget.registry.websiteSlides;\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { SlideShareDialog } from './public/components/slide_share_dialog/slide_share_dialog';\nimport { browser } from '@web/core/browser/browser';\n\n\npublicWidget.registry.websiteSlidesShare = publicWidget.Widget.extend({\n    selector: '#wrapwrap',\n    events: {\n        'click .o_wslides_share': '_onClickShareSlide',\n    },\n\n    getDocumentMaxPage() {\n        const iframe = document.querySelector(\"iframe.o_wslides_iframe_viewer\");\n        const iframeDocument = iframe.contentWindow.document;\n        return parseInt(iframeDocument.querySelector(\"#page_count\").innerText);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    _onClickShareSlide: function (ev) {\n        ev.stopPropagation();\n        ev.preventDefault();\n        const data = ev.currentTarget.dataset;\n        this.call(\"dialog\", \"add\", SlideShareDialog, {\n            category: data.category,\n            documentMaxPage: data.category == 'document' && this.getDocumentMaxPage(),\n            emailSharing: data.emailSharing === 'True',\n            embedCode: data.embedCode,\n            id: parseInt(data.id),\n            isChannel: data.isChannel === 'True',\n            name: data.name,\n            url: data.url,\n        });\n    },\n});\n\npublicWidget.registry.websiteSlidesEmbedShare = publicWidget.Widget.extend({\n    selector: '.oe_slide_js_embed_code_widget',\n    events: {\n        'click .o_embed_clipboard_button': '_onShareLinkCopy',\n    },\n\n    _onShareLinkCopy: async function (ev) {\n        ev.preventDefault();\n        const $clipboardBtn = $(ev.currentTarget);\n        $clipboardBtn.tooltip({title: \"Copied!\", trigger: \"manual\", placement: \"bottom\"});\n        var share_embed_el = this.$('#wslides_share_embed_id_' + $clipboardBtn[0].id.split('id_')[1]);\n        await browser.navigator.clipboard.writeText(share_embed_el.val() || '');\n        $clipboardBtn.tooltip('show');\n        setTimeout(function () {\n            $clipboardBtn.tooltip(\"hide\");\n        }, 800);\n    },\n});\n\nexport const WebsiteSlidesShare = publicWidget.registry.websiteSlidesShare;\nexport const WebsiteSlidesEmbedShare = publicWidget.registry.websiteSlidesEmbedShare;\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { SlideUploadDialog } from \"@website_slides/js/public/components/slide_upload_dialog/slide_upload_dialog\";\n\npublicWidget.registry.websiteSlidesUpload = publicWidget.Widget.extend({\n    selector: '.o_wslides_js_slide_upload',\n    events: {\n        'click': '_onUploadClick',\n    },\n\n    /**\n     * Automatically opens the upload dialog if requested from query string.\n     * If openModal is defined ( === '' ), opens the category selection dialog.\n     * If openModal is a category name, opens the category's upload dialog.\n     *\n     * @override\n     */\n    start: function () {\n        if ('openModal' in this.$el.data()) {\n            this._openDialog(this.$el);\n            this.$el.data('openModal', false);\n        }\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _openDialog: function ($element) {\n        const dataset = $element.data();\n        this.call(\"dialog\", \"add\", SlideUploadDialog, {\n            categoryId: dataset.categoryId,\n            channelId: dataset.channelId,\n            canPublish: dataset.canPublish === \"True\",\n            canUpload: dataset.canUpload === \"True\",\n            modulesToInstall: dataset.modulesToInstall || [],\n            openModal: dataset.openModal,\n        });\n    },\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onUploadClick: function (ev) {\n        ev.preventDefault();\n        this._openDialog($(ev.currentTarget));\n    },\n});\n\nexport default {\n    websiteSlidesUpload: publicWidget.registry.websiteSlidesUpload\n};\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { CategoryAddDialog } from \"@website_slides/js/public/components/category_add_dialog/category_add_dialog\";\n\npublicWidget.registry.websiteSlidesCategoryAdd = publicWidget.Widget.extend({\n    selector: '.o_wslides_js_slide_section_add',\n    events: {\n        'click': '_onAddSectionClick',\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _openDialog: function (channelId) {\n        this.call(\"dialog\", \"add\", CategoryAddDialog, {\n            title: _t(\"Add a section\"),\n            confirmLabel: _t(\"Save\"),\n            confirm: ({ formEl }) => {\n                if (!formEl.checkValidity()) {\n                    return false;\n                }\n                formEl.classList.add(\"was-validated\");\n                formEl.submit();\n                return true;\n            },\n            cancelLabel: _t(\"Back\"),\n            cancel: () => {},\n            channelId,\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onAddSectionClick: function (ev) {\n        ev.preventDefault();\n        this._openDialog($(ev.currentTarget).attr('channel_id'));\n    },\n});\n\nexport default {\n    websiteSlidesCategoryAdd: publicWidget.registry.websiteSlidesCategoryAdd\n};\n", "/** @odoo-module **/\n\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npublicWidget.registry.websiteSlidesCategoryDelete = publicWidget.Widget.extend({\n    selector: \".o_wslides_js_category_delete\",\n    events: {\n        click: \"_onClickDeleteCateogry\",\n    },\n\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {MouseEvent} ev\n     */\n    _onClickDeleteCateogry(ev) {\n        const categoryId = parseInt(ev.currentTarget.dataset.categoryId);\n        this.call(\"dialog\", \"add\", ConfirmationDialog, {\n            title: _t(\"Delete Category\"),\n            body: _t(\"Are you sure you want to delete this category?\"),\n            confirmLabel: _t(\"Delete\"),\n            confirm: async () => {\n                /**\n                 * Calls 'unlink' method on slides.slide to delete the category and\n                 * reloads page after deletion to re-arrange the content on UI\n                 */\n                await this.orm.unlink(\"slide.slide\", [categoryId]);\n                window.location.reload();\n            },\n            cancel: () => {},\n        });\n    },\n});\n\nexport default {\n    websiteSlidesCategoryDelete: publicWidget.registry.websiteSlidesCategoryDelete,\n};\n", "/** @odoo-module **/\n\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { rpc } from \"@web/core/network/rpc\";\n\npublicWidget.registry.websiteSlidesSlideArchive = publicWidget.Widget.extend({\n    selector: \".o_wslides_js_slide_archive\",\n    events: {\n        click: \"_onArchiveSlideClick\",\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _openDialog: function ($slideTarget) {\n        const slideId = $slideTarget.data(\"slideId\");\n        this.call(\"dialog\", \"add\", ConfirmationDialog, {\n            title: _t(\"Archive Content\"),\n            body: _t(\"Are you sure you want to archive this content?\"),\n            confirmLabel: _t(\"Archive\"),\n            confirm: async () => {\n                /**\n                 * Calls 'archive' on slide controller and then visually removes the slide dom element\n                 */\n                const isArchived = await rpc(\"/slides/slide/archive\", {\n                    slide_id: slideId,\n                });\n                if (isArchived) {\n                    $slideTarget.closest(\".o_wslides_slides_list_slide\").remove();\n                    $(\".o_wslides_slide_list_category\").each(function () {\n                        var $categoryHeader = $(this).find(\".o_wslides_slide_list_category_header\");\n                        var categorySlideCount = $(this).find(\n                            \".o_wslides_slides_list_slide:not(.o_not_editable)\"\n                        ).length;\n                        var $emptyFlagContainer = $categoryHeader\n                            .find(\".o_wslides_slides_list_drag\")\n                            .first();\n                        var $emptyFlag = $emptyFlagContainer.find(\"small\");\n                        if (categorySlideCount === 0 && $emptyFlag.length === 0) {\n                            $emptyFlagContainer.append(\n                                $(\"<small>\", {\n                                    class: \"ms-1 text-muted fw-bold\",\n                                    text: _t(\"(empty)\"),\n                                })\n                            );\n                        }\n                    });\n                }\n            },\n            cancel: () => {},\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onArchiveSlideClick: function (ev) {\n        ev.preventDefault();\n        var $slideTarget = $(ev.currentTarget);\n        this._openDialog($slideTarget);\n    },\n});\n\nexport default {\n    websiteSlidesSlideArchive: publicWidget.registry.websiteSlidesSlideArchive,\n};\n", "/** @odoo-module **/\n\n    import publicWidget from '@web/legacy/js/public/public_widget';\n    import { rpc } from \"@web/core/network/rpc\";\n\n    publicWidget.registry.websiteSlidesSlideToggleIsPreview = publicWidget.Widget.extend({\n        selector: '.o_wslides_js_slide_toggle_is_preview',\n        events: {\n            'click': '_onPreviewSlideClick',\n        },\n\n        _toggleSlidePreview: function($slideTarget) {\n            rpc('/slides/slide/toggle_is_preview', {\n                slide_id: $slideTarget.data('slideId')\n            }).then(function (isPreview) {\n                if (isPreview) {\n                    $slideTarget.removeClass('text-bg-light badge-hide border');\n                    $slideTarget.addClass('text-bg-success');\n                } else {\n                    $slideTarget.removeClass('text-bg-success');\n                    $slideTarget.addClass('text-bg-light badge-hide border');\n                }\n            });\n        },\n\n        _onPreviewSlideClick: function (ev) {\n            ev.preventDefault();\n            this._toggleSlidePreview($(ev.currentTarget));\n        },\n    });\n\n    export default {\n        websiteSlidesSlideToggleIsPreview: publicWidget.registry.websiteSlidesSlideToggleIsPreview\n    };\n", "/** @odoo-module **/\n\nimport { sprintf } from '@web/core/utils/strings';\nimport { _t } from \"@web/core/l10n/translation\";\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { rpc } from \"@web/core/network/rpc\";\nimport '@website_slides/js/slides';\n\nvar SlideLikeWidget = publicWidget.Widget.extend({\n    events: {\n        'click .o_wslides_js_slide_like_up': '_onClickUp',\n        'click .o_wslides_js_slide_like_down': '_onClickDown',\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Object} $el\n     * @param {String} message\n     */\n    _popoverAlert: function ($el, message) {\n        $el.popover({\n            trigger: 'focus',\n            delay: {'hide': 300},\n            placement: 'bottom',\n            container: 'body',\n            html: true,\n            content: function () {\n                return message;\n            }\n        }).popover('show');\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onClick: function (slideId, voteType) {\n        var self = this;\n        rpc('/slides/slide/like', {\n            slide_id: slideId,\n            upvote: voteType === 'like',\n        }).then(function (data) {\n            if (! data.error) {\n                const $likesBtn = self.$('span.o_wslides_js_slide_like_up');\n                const $likesIcon = $likesBtn.find('i.fa');\n                const $dislikesBtn = self.$('span.o_wslides_js_slide_like_down');\n                const $dislikesIcon = $dislikesBtn.find('i.fa');\n\n                // update 'thumbs-up' button with latest state\n                $likesBtn.data('user-vote', data.user_vote);\n                $likesBtn.find('span').text(data.likes);\n                $likesIcon.toggleClass(\"fa-thumbs-up\", data.user_vote === 1);\n                $likesIcon.toggleClass(\"fa-thumbs-o-up\", data.user_vote !== 1);\n                // update 'thumbs-down' button with latest state\n                $dislikesBtn.data('user-vote', data.user_vote);\n                $dislikesBtn.find('span').text(data.dislikes);\n                $dislikesIcon.toggleClass(\"fa-thumbs-down\", data.user_vote === -1);\n                $dislikesIcon.toggleClass(\"fa-thumbs-o-down\", data.user_vote !== -1);\n            } else {\n                if (data.error === 'public_user') {\n                    const message = data.error_signup_allowed ?\n                        _t('Please <a href=\"/web/login?redirect=%(url)s\">login</a> or <a href=\"/web/signup?redirect=%(url)s\">create an account</a> to vote for this lesson') :\n                        _t('Please <a href=\"/web/login?redirect=%(url)s\">login</a> to vote for this lesson');\n                    self._popoverAlert(self.$el, sprintf(message, { url: encodeURIComponent(document.URL) }));\n                } else if (data.error === 'slide_access') {\n                    self._popoverAlert(self.$el, _t('You don\\'t have access to this lesson'));\n                } else if (data.error === 'channel_membership_required') {\n                    self._popoverAlert(self.$el, _t('You must be member of this course to vote'));\n                } else if (data.error === 'channel_comment_disabled') {\n                    self._popoverAlert(self.$el, _t('Votes and comments are disabled for this course'));\n                } else if (data.error === 'channel_karma_required') {\n                    self._popoverAlert(self.$el, _t('You don\\'t have enough karma to vote'));\n                } else {\n                    self._popoverAlert(self.$el, _t('Unknown error'));\n                }\n            }\n        });\n    },\n\n    _onClickUp: function (ev) {\n        var slideId = $(ev.currentTarget).data('slide-id');\n        return this._onClick(slideId, 'like');\n    },\n\n    _onClickDown: function (ev) {\n        var slideId = $(ev.currentTarget).data('slide-id');\n        return this._onClick(slideId, 'dislike');\n    },\n});\n\npublicWidget.registry.websiteSlidesSlideLike = publicWidget.Widget.extend({\n    selector: '#wrapwrap',\n\n    /**\n     * @override\n     * @param {Object} parent\n     */\n    start: function () {\n        var self = this;\n        var defs = [this._super.apply(this, arguments)];\n        $('.o_wslides_js_slide_like').each(function () {\n            defs.push(new SlideLikeWidget(self).attachTo($(this)));\n        });\n        return Promise.all(defs);\n    },\n});\n\nexport default {\n    slideLikeWidget: SlideLikeWidget,\n    websiteSlidesSlideLike: publicWidget.registry.websiteSlidesSlideLike\n};\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { session } from \"@web/session\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { rpc } from \"@web/core/network/rpc\";\n\n/**\n * Global widget for both fullscreen view and non-fullscreen view of a slide course.\n * Contains general methods to update the UI elements (progress bar, sidebar...) as well\n * as method to mark the slide as completed / uncompleted.\n */\nexport const SlideCoursePage = publicWidget.Widget.extend({\n    events: {\n        'click button.o_wslides_button_complete': '_onClickComplete',\n    },\n\n    custom_events: {\n        'slide_completed': '_onSlideCompleted',\n        'slide_mark_completed': '_onSlideMarkCompleted',\n    },\n\n    /**\n     * Collapse the next category when the current one has just been completed\n     */\n    collapseNextCategory: function (nextCategoryId) {\n        const categorySection = document.getElementById(`category-collapse-${nextCategoryId}`);\n        if (categorySection?.getAttribute('aria-expanded') === 'false') {\n            categorySection.setAttribute('aria-expanded', true);\n            document.querySelector(`ul[id=collapse-${nextCategoryId}]`).classList.add('show');\n        }\n    },\n\n    /**\n     * Greens up the bullet when the slide is completed\n     *\n     * @public\n     * @param {Object} slide\n     * @param {Boolean} completed\n     */\n    toggleCompletionButton: function (slide, completed = true) {\n        const $button = this.$(`.o_wslides_sidebar_done_button[data-id=\"${slide.id}\"]`);\n\n        if (!$button.length) {\n            return;\n        }\n\n        const newButton = renderToElement('website.slides.sidebar.done.button', {\n            slideId: slide.id,\n            uncompletedIcon: $button.data('uncompletedIcon') ?? 'fa-circle-thin',\n            slideCompleted: completed ? 1 : 0,\n            canSelfMarkUncompleted: slide.canSelfMarkUncompleted,\n            canSelfMarkCompleted: slide.canSelfMarkCompleted,\n            isMember: slide.isMember,\n        });\n        $button.replaceWith(newButton);\n    },\n\n    /**\n     * Updates the progressbar whenever a lesson is completed\n     *\n     * @public\n     * @param {Integer} channelCompletion\n     */\n    updateProgressbar: function (channelCompletion) {\n        const completion = Math.min(100, channelCompletion);\n\n        const $completed = $('.o_wslides_channel_completion_completed');\n        const $progressbar = $('.o_wslides_channel_completion_progressbar');\n\n        if (completion < 100) {\n            // Hide the \"Completed\" text and show the progress bar\n            $completed.addClass('d-none');\n            $progressbar.removeClass('d-none').addClass('d-flex');\n        } else {\n            // Hide the progress bar and show the \"Completed\" text\n            $completed.removeClass('d-none');\n            $progressbar.addClass('d-none').removeClass('d-flex');\n        }\n\n        $progressbar.find('.progress-bar').css('width', `${completion}%`);\n        $progressbar.find('.o_wslides_progress_percentage').text(completion);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Once the completion conditions are filled,\n     * rpc call to set the relation between the slide and the user as \"completed\"\n     *\n     * @private\n     * @param {Object} slide: slide to set as completed\n     * @param {Boolean} completed: true to mark the slide as completed\n     *     false to mark the slide as not completed\n     */\n    _toggleSlideCompleted: async function (slide, completed = true) {\n        if (!!slide.completed === !!completed || !slide.isMember || !slide.canSelfMarkCompleted) {\n            // no useless RPC call\n            return;\n        }\n\n        const data = await rpc(\n            `/slides/slide/${completed ? 'set_completed' : 'set_uncompleted'}`,\n            {slide_id: slide.id},\n        );\n\n        this.toggleCompletionButton(slide, completed);\n        this.updateProgressbar(data.channel_completion);\n        if (data.next_category_id) {\n            this.collapseNextCategory(data.next_category_id);\n        }\n    },\n    /**\n     * Retrieve the slide data corresponding to the slide id given in argument.\n     * This method used the \"slide_sidebar_done_button\" template.\n     *\n     * @private\n     * @param {Integer} slideId\n     */\n    _getSlide: function (slideId) {\n        return $(`.o_wslides_sidebar_done_button[data-id=\"${slideId}\"]`).data();\n    },\n\n    //--------------------------------------------------------------------------\n    // Handler\n    //--------------------------------------------------------------------------\n    /**\n     * We clicked on the \"done\" button.\n     * It will make a RPC call to update the slide state and update the UI.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onClickComplete: function (ev) {\n        ev.stopPropagation();\n        ev.preventDefault();\n\n        const $button = $(ev.currentTarget).closest('.o_wslides_sidebar_done_button');\n\n        const slideData = $button.data();\n        const isCompleted = Boolean(slideData.completed);\n\n        this._toggleSlideCompleted(slideData, !isCompleted);\n    },\n\n    /**\n     * The slide has been completed, update the UI\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onSlideCompleted: function (ev) {\n        const slideId = ev.data.slideId;\n        const completed = ev.data.completed;\n        const slide = this._getSlide(slideId);\n        if (slide) {\n            // Just joined the course (e.g. When \"Submit & Join\" action), update the UI\n            this.toggleCompletionButton(slide, completed);\n        }\n        this.updateProgressbar(ev.data.channelCompletion);\n    },\n\n    /**\n     * Make a RPC call to complete the slide then update the UI\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onSlideMarkCompleted: function (ev) {\n        if (!session.is_website_user) { // no useless RPC call\n            const slide = this._getSlide(ev.data.id);\n            this._toggleSlideCompleted(slide, true);\n        }\n    }\n});\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { SlideCoursePage } from '@website_slides/js/slides_course_page';\n\npublicWidget.registry.websiteSlidesCourseSlidesList = SlideCoursePage.extend({\n    selector: '.o_wslides_slides_list',\n\n    start: function () {\n        this._super.apply(this,arguments);\n\n        this.channelId = this.$el.data('channelId');\n        this.bindedSortable = [];\n\n        this._updateHref();\n        this._bindSortable();\n    },\n\n    destroy() {\n        this._unbindSortable();\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------,\n\n    /**\n     * Bind the sortable jQuery widget to both\n     * - course sections\n     * - course slides\n     *\n     * @private\n     */\n    _bindSortable: function () {\n        const sortableBaseParam = {\n            clone: false,\n            placeholderClasses: ['o_wslides_slides_list_slide_hilight', 'position-relative', 'mb-1'],\n            onDrop: this._reorderSlides.bind(this),\n            applyChangeOnDrop: true\n        };\n\n        const container = this.el.querySelector('ul.o_wslides_js_slides_list_container');\n        this.bindedSortable.push(this.call(\n            \"sortable\",\n            \"create\",\n            {\n                ...sortableBaseParam,\n                ref: { el: container },\n                elements: \".o_wslides_slide_list_category\",\n                handle: \".o_wslides_slide_list_category_header .o_wslides_slides_list_drag\",\n                sortableId: \"category\",\n            },\n        ).enable());\n\n        this.bindedSortable.push(this.call(\n            \"sortable\",\n            \"create\",\n            {\n                ...sortableBaseParam,\n                ref: { el: container },\n                elements: \".o_wslides_slides_list_slide:not(.o_wslides_js_slides_list_empty):not(.o_not_editable)\",\n                handle: \".o_wslides_slides_list_drag\",\n                connectGroups: true,\n                groups: \".o_wslides_js_slides_list_container ul\",\n                sortableId: \"list\",\n            },\n        ).enable());\n    },\n\n    _unbindSortable: function () {\n        this.bindedSortable.forEach(sortable => sortable.cleanup());\n    },\n\n    /**\n     * This method will check that a section is empty/not empty\n     * when the slides are reordered and show/hide the\n     * \"Empty category\" placeholder.\n     *\n     * @private\n     */\n    _checkForEmptySections: function (){\n        this.$('.o_wslides_slide_list_category').each(function (){\n            var $categoryHeader = $(this).find('.o_wslides_slide_list_category_header');\n            var categorySlideCount = $(this).find('.o_wslides_slides_list_slide:not(.o_not_editable)').length;\n            var $emptyFlagContainer = $categoryHeader.find('.o_wslides_slides_list_drag').first();\n            var $emptyFlag = $emptyFlagContainer.find('small');\n            if (categorySlideCount === 0 && $emptyFlag.length === 0){\n                $emptyFlagContainer.append($('<small>', {\n                    'class': \"ms-1 text-muted fw-bold\",\n                    text: _t(\"(empty)\")\n                }));\n            } else if (categorySlideCount > 0 && $emptyFlag.length > 0){\n                $emptyFlag.remove();\n            }\n        });\n    },\n\n    _getSlides: function (){\n        var categories = [];\n        this.$('.o_wslides_js_list_item').each(function (){\n            categories.push(parseInt($(this).data('slideId')));\n        });\n        return categories;\n    },\n    _reorderSlides: function (){\n        var self = this;\n        rpc('/web/dataset/resequence', {\n            model: \"slide.slide\",\n            ids: self._getSlides(),\n        }).then(function (res) {\n            self._checkForEmptySections();\n        });\n    },\n\n    /**\n     * Change links href to fullscreen mode for SEO.\n     *\n     * Specifications demand that links are generated (xml) without the \"fullscreen\"\n     * parameter for SEO purposes.\n     *\n     * This method then adds the parameter as soon as the page is loaded.\n     *\n     * @private\n     */\n    _updateHref: function () {\n        this.$(\".o_wslides_js_slides_list_slide_link\").each(function (){\n            var href = $(this).attr('href');\n            var operator = href.indexOf('?') !== -1 ? '&' : '?';\n            $(this).attr('href', href + operator + \"fullscreen=1\");\n        });\n    }\n});\n\nexport default publicWidget.registry.websiteSlidesCourseSlidesList;\n", "/** @odoo-module **/\n\n/* global YT, Vimeo */\n\n    import publicWidget from '@web/legacy/js/public/public_widget';\n    import { renderToElement } from \"@web/core/utils/render\";\n    import { session } from \"@web/session\";\n    import { Quiz } from '@website_slides/js/slides_course_quiz';\n    import { SlideCoursePage } from '@website_slides/js/slides_course_page';\n    import { unhideConditionalElements } from '@website/js/content/inject_dom';\n    import { SlideShareDialog } from './public/components/slide_share_dialog/slide_share_dialog';\n    import '@website_slides/js/slides_course_join';\n    import { SIZES, utils as uiUtils } from \"@web/core/ui/ui_service\";\n    import { rpc } from \"@web/core/network/rpc\";\n\n    import { markup } from \"@odoo/owl\";\n\n    /**\n     * Helper: Get the slide dict matching the given criteria\n     *\n     * @private\n     * @param {Array<Object>} slideList List of dict reprensenting a slide\n     * @param {[string] : any} matcher\n     */\n    var findSlide = function (slideList, matcher) {\n        return slideList.find((slide) => {\n            return Object.keys(matcher).every((key) => matcher[key] === slide[key]);\n        });\n    };\n\n    /**\n     * This widget is responsible of display Youtube Player\n     *\n     * The widget will trigger an event `change_slide` when the video is at\n     * its end, and `slide_completed` when the player is at 30 sec before the\n     * end of the video (30 sec before is considered as completed).\n     */\n    var VideoPlayerYouTube = publicWidget.Widget.extend({\n        template: 'website.slides.fullscreen.video.youtube',\n        youtubeUrl: 'https://www.youtube.com/iframe_api',\n\n        init: function (parent, slide) {\n            this.slide = slide;\n            return this._super.apply(this, arguments);\n        },\n        start: function (){\n            var self = this;\n            return Promise.all([this._super.apply(this, arguments), this._loadYoutubeAPI()]).then(function() {\n                self._setupYoutubePlayer();\n            });\n        },\n        _loadYoutubeAPI: function () {\n            var self = this;\n            var prom = new Promise(function (resolve, reject) {\n                if ($(document).find('script[src=\"' + self.youtubeUrl + '\"]').length === 0) {\n                    var $youtubeElement = $('<script/>', {src: self.youtubeUrl});\n                    $(document.head).append($youtubeElement);\n\n                    // function called when the Youtube asset is loaded\n                    // see https://developers.google.com/youtube/iframe_api_reference#Requirements\n                    window.onYouTubeIframeAPIReady = function () {\n                        resolve();\n                    };\n                } else {\n                    resolve();\n                }\n            });\n            return prom;\n        },\n        /**\n         * Links the youtube api to the iframe present in the template\n         *\n         * @private\n         */\n        _setupYoutubePlayer: function (){\n            this.player = new YT.Player('youtube-player' + this.slide.id, {\n                playerVars: {\n                    'autoplay': 1,\n                    'origin': window.location.origin\n                },\n                events: {\n                    'onStateChange': this._onPlayerStateChange.bind(this)\n                }\n            });\n        },\n        /**\n         * Specific method of the youtube api.\n         * Whenever the player starts playing/pausing/buffering/..., a setinterval is created.\n         * This setinterval is used to check te user's progress in the video.\n         * Once the user reaches a particular time in the video (30s before end), the slide will be considered as completed\n         * if the video doesn't have a mini-quiz.\n         * This method also allows to automatically go to the next slide (or the quiz associated to the current\n         * video) once the video is over\n         *\n         * @private\n         * @param {*} event\n         */\n        _onPlayerStateChange: function (event){\n            var self = this;\n\n            if (self.slide.completed) {\n                return;\n            }\n\n            if (event.data !== YT.PlayerState.ENDED) {\n                if (!event.target.getCurrentTime) {\n                    return;\n                }\n\n                if (self.tid) {\n                    clearInterval(self.tid);\n                }\n\n                self.currentVideoTime = event.target.getCurrentTime();\n                self.totalVideoTime = event.target.getDuration();\n                self.tid = setInterval(function (){\n                    self.currentVideoTime += 1;\n                    if (self.totalVideoTime && self.currentVideoTime > self.totalVideoTime - 30){\n                        clearInterval(self.tid);\n                        if (self.slide.isMember && !self.slide.hasQuestion && !self.slide.completed){\n                            self.trigger_up('slide_mark_completed', self.slide);\n                        }\n                    }\n                }, 1000);\n            } else {\n                if (self.tid) {\n                    clearInterval(self.tid);\n                }\n                this.player = undefined;\n                if (this.slide.hasNext) {\n                    this.trigger_up('slide_go_next');\n                }\n            }\n        },\n    });\n\n    /**\n     * This widget is responsible of loading the Vimeo video.\n     *\n     * Similarly to the YouTube implementation, the widget will trigger an event `change_slide` when\n     * the video is at its end, and `slide_completed` when the player is at 30 sec before the end of\n     * the video (30 sec before is considered as completed).\n     *\n     * See https://developer.vimeo.com/player/sdk/reference for all the API documentation.\n     */\n    var VideoPlayerVimeo = publicWidget.Widget.extend({\n        template: 'website.slides.fullscreen.video.vimeo',\n        vimeoScriptUrl: 'https://player.vimeo.com/api/player.js',\n\n        init: function (parent, slide) {\n            this.slide = slide;\n            return this._super.apply(this, arguments);\n        },\n\n        /**\n         * Loads the Vimeo JS API that allows interfacing with the iframe viewer.\n         * (We only load the API if not already loaded).\n         *\n         * @returns {Promise}\n         */\n        willStart: function () {\n            var self = this;\n            var vimeoAPIPromise = new Promise(function (resolve, reject) {\n                if ($(document).find('script[src=\"' + self.vimeoScriptUrl + '\"]').length === 0) {\n                    $.ajax({\n                        url: self.vimeoScriptUrl,\n                        dataType: 'script',\n                        success: function () {resolve();}\n                    });\n                } else {\n                    resolve();\n                }\n            });\n\n            return Promise.all([this._super.apply(this, arguments), vimeoAPIPromise]);\n        },\n\n        start: function () {\n            return this._super.apply(arguments).then(this._setupVideoPlayer.bind(this));\n        },\n\n        //--------------------------------------------------------------------------\n        // Private\n        //--------------------------------------------------------------------------\n\n        /**\n         * Instantiate the Vimeo player and register the various events.\n         */\n        _setupVideoPlayer: async function () {\n            this.player = new Vimeo.Player(this.$('iframe')[0]);\n            this.videoDuration = await this.player.getDuration();\n            this.player.on('timeupdate', this._onVideoTimeUpdate.bind(this));\n            this.player.on('ended', this._onVideoEnded.bind(this));\n        },\n\n        //--------------------------------------------------------------------------\n        // Handlers\n        //--------------------------------------------------------------------------\n\n        /**\n         * When the player triggers the 'ended' event, we go to the next slide if there is one.\n         *\n         * See https://developer.vimeo.com/player/sdk/reference#ended for more information\n         */\n        _onVideoEnded: function () {\n            if (this.slide.hasNext) {\n                this.trigger_up('slide_go_next', this.slide);\n            }\n        },\n\n        /**\n         * Every time the video changes position, both while viewing and also when seeking manually,\n         * Vimeo triggers this handy 'timeupdate' event.\n         * We use it to set the slide as completed as soon as we reach the end (30 last seconds).\n         *\n         * See https://developer.vimeo.com/player/sdk/reference#timeupdate for more information\n         *\n         * @param {Object} eventData the 'timeupdate' event data\n         */\n         _onVideoTimeUpdate: async function (eventData) {\n            if (eventData.seconds > (this.videoDuration - 30)) {\n                if (this.slide.isMember && !this.slide.hasQuestion && !this.slide.completed){\n                    this.trigger_up('slide_mark_completed', this.slide);\n                }\n            }\n        }\n    });\n\n\n    /**\n     * This widget is responsible of navigation for one slide to another:\n     *  - by clicking on any slide list entry\n     *  - by mouse click (next / prev)\n     *  - by recieving the order to go to prev/next slide (`goPrevious` and `goNext` public methods)\n     *\n     * The widget will trigger an event `change_slide` with\n     * the `slideId` and `isMiniQuiz` as data.\n     */\n    var Sidebar = publicWidget.Widget.extend({\n        events: {\n            'click .o_wslides_fs_sidebar_list_item .o_wslides_fs_slide_name': '_onClickTab',\n        },\n        init: function (parent, slideList, defaultSlide) {\n            var result = this._super.apply(this, arguments);\n            this.slideEntries = slideList;\n            this._slideEntry = defaultSlide;\n            return result;\n        },\n        start: function () {\n            var self = this;\n            return this._super.apply(this, arguments).then(function () {\n                $(document).keydown(self._onKeyDown.bind(self));\n            });\n        },\n        destroy: function () {\n            $(document).unbind('keydown', this._onKeyDown.bind(this));\n            return this._super.apply(this, arguments);\n        },\n        //--------------------------------------------------------------------------\n        // Public\n        //--------------------------------------------------------------------------\n        /**\n         * Change the current slide with the next one (if there is one).\n         *\n         * @public\n         */\n        goNext: function () {\n            var currentIndex = this._getCurrentIndex();\n            if (currentIndex < this.slideEntries.length-1) {\n                this._updateSlideEntry(this.slideEntries[currentIndex + 1]);\n            }\n        },\n        /**\n         * Change the current slide with the previous one (if there is one).\n         *\n         * @public\n         */\n        goPrevious: function () {\n            var currentIndex = this._getCurrentIndex();\n            if (currentIndex >= 1) {\n                this._updateSlideEntry(this.slideEntries[currentIndex - 1]);\n            }\n        },\n\n        //--------------------------------------------------------------------------\n        // Private\n        //--------------------------------------------------------------------------\n        /**\n         * Get the index of the current slide entry (slide and/or quiz)\n         */\n        _getCurrentIndex: function () {\n            const slide = this._slideEntry;\n            var currentIndex = this.slideEntries.findIndex(entry =>{\n                return entry.id === slide.id && entry.isQuiz === slide.isQuiz;\n            });\n            return currentIndex;\n        },\n        //--------------------------------------------------------------------------\n        // Handler\n        //--------------------------------------------------------------------------\n        /**\n         * Handler called whenever the user clicks on a sub-quiz which is linked to a slide.\n         * This does NOT handle the case of a slide of category \"quiz\".\n         * By going through this handler, the widget will be able to determine that it has to render\n         * the associated quiz and not the main content.\n         *\n         * @private\n         * @param {*} ev\n         */\n        _onClickMiniQuiz: function (ev) {\n            var slideID = parseInt($(ev.currentTarget).data().slide_id);\n            this._updateSlideEntry({\n                slideID: slideID,\n                isMiniQuiz: true\n            });\n            this.trigger_up('change_slide', this._slideEntry);\n        },\n        /**\n         * Handler called when the user clicks on a normal slide tab\n         *\n         * @private\n         * @param {*} ev\n         */\n        _onClickTab: function (ev) {\n            ev.stopPropagation();\n            const $elem = $(ev.currentTarget).closest('.o_wslides_fs_sidebar_list_item');\n            if ($elem.data('canAccess') === 'True') {\n                var isQuiz = $elem.data('isQuiz');\n                var slideID = parseInt($elem.data('id'));\n                var slide = findSlide(this.slideEntries, {id: slideID, isQuiz: isQuiz});\n                this._updateSlideEntry(slide);\n            }\n        },\n        /**\n         * Actively changes the active tab in the sidebar so that it corresponds\n         * the slide currently displayed\n         *\n         * @private\n         * @param {Object} slide\n         */\n        _updateSlideEntry: function (slide) {\n            if (this._slideEntry === slide) {\n                return;\n            }\n            this._slideEntry = slide;\n            this.$('.o_wslides_fs_sidebar_list_item.active').removeClass('active');\n            var selector = '.o_wslides_fs_sidebar_list_item[data-id='+slide.id+'][data-is-quiz!=\"1\"]';\n\n            this.$(selector).addClass('active');\n            this.trigger_up('change_slide', this._slideEntry);\n        },\n\n        /**\n         * Binds left and right arrow to allow the user to navigate between slides\n         *\n         * @param {*} ev\n         * @private\n         */\n        _onKeyDown: function (ev){\n            switch (ev.key){\n                case \"ArrowLeft\":\n                    this.goPrevious();\n                    break;\n                case \"ArrowRight\":\n                    this.goNext();\n                    break;\n            }\n        },\n    });\n\n    /**\n     * This widget's purpose is to show content of a course, naviguating through contents\n     * and correclty display it. It also handle slide completion, course progress, ...\n     *\n     * This widget is rendered sever side, and attached to the existing DOM.\n     */\n    var Fullscreen = SlideCoursePage.extend({\n        events: Object.assign({}, SlideCoursePage.prototype.events, {\n            'click .o_wslides_fs_toggle_sidebar': '_onClickToggleSidebar',\n            'click .o_wslides_fs_share': '_onClickShareSlide',\n        }),\n        custom_events: Object.assign({}, SlideCoursePage.prototype.custom_events, {\n            'change_slide': '_onChangeSlideRequest',\n            'slide_go_next': '_onSlideGoToNext',\n        }),\n        /**\n        * @override\n        * @param {Object} el\n        * @param {Object} slides Contains the list of all slides of the course\n        * @param {integer} defaultSlideId Contains the ID of the slide requested by the user\n        */\n        init: function (parent, slides, defaultSlideId, channelData) {\n            var result = this._super.apply(this,arguments);\n            this.initialSlideID = defaultSlideId;\n            this.slides = this._preprocessSlideData(slides);\n            this.channel = channelData;\n            var slide;\n            const urlParams = new URL(window.location).searchParams;\n            if (defaultSlideId) {\n                slide = findSlide(this.slides, {id: defaultSlideId, isQuiz: String(urlParams.get(\"quiz\")) === \"1\" });\n            } else {\n                slide = this.slides[0];\n            }\n\n            this._slideValue = slide;\n\n            this.sidebar = new Sidebar(this, this.slides, slide);\n            return result;\n        },\n        /**\n         * @override\n         */\n        start: function () {\n            var self = this;\n            this._toggleSidebar();\n            const backendNavEl = document.querySelector('.o_frontend_to_backend_nav');\n            if (backendNavEl) {\n                backendNavEl.remove();\n            }\n            return this._super.apply(this, arguments).then(function () {\n                return self._onChangeSlide(); // trigger manually once DOM ready, since slide content is not rendered server side\n            });\n        },\n        /**\n         * Extended to attach sub widget to sub DOM. This might be experimental but\n         * seems working fine.\n         *\n         * @override\n         */\n        attachTo: function (){\n            var defs = [this._super.apply(this, arguments)];\n            defs.push(this.sidebar.attachTo(this.$('.o_wslides_fs_sidebar')));\n            return $.when.apply($, defs);\n        },\n        //--------------------------------------------------------------------------\n        // Private\n        //--------------------------------------------------------------------------\n        /**\n         * Fetches content with an rpc call for slides of category \"article\"\n         *\n         * @private\n         */\n        _fetchHtmlContent: function () {\n            const currentSlide = this._slideValue;\n            return rpc(\"/slides/slide/get_html_content\", {\n                'slide_id': currentSlide.id\n            }).then(function (data){\n                if (data.html_content) {\n                    currentSlide.htmlContent = data.html_content;\n                }\n            });\n        },\n        /**\n        * Fetches slide content depending on its category.\n        * If the slide doesn't need to fetch any content, return a resolved deferred\n        *\n        * @private\n        */\n        _fetchSlideContent: function () {\n            const slide = this._slideValue;\n            if (slide.category === 'article' && !slide.isQuiz) {\n                return this._fetchHtmlContent();\n            }\n            return Promise.resolve();\n        },\n        getDocumentMaxPage() {\n            const iframe = document.querySelector(\"iframe.o_wslides_iframe_viewer\");\n            const iframeDocument = iframe.contentWindow.document;\n            return parseInt(iframeDocument.querySelector(\"#page_count\").innerText);\n        },\n        /**\n         * Extend the slide data list to add informations about rendering method, and other\n         * specific values according to their slide_category.\n         */\n        _preprocessSlideData: function (slidesDataList) {\n            slidesDataList.forEach(function (slideData, index) {\n                // compute hasNext slide\n                slideData.hasNext = index < slidesDataList.length-1;\n                // compute embed url\n                if (slideData.category === 'video' && slideData.videoSourceType !== 'vimeo') {\n                    slideData.embedCode = $(slideData.embedCode).attr('src') || \"\"; // embedCode contains an iframe tag, where src attribute is the url (youtube or embed document from odoo)\n                    var separator = slideData.embedCode.indexOf(\"?\") !== -1 ? \"&\" : \"?\";\n                    var scheme = slideData.embedCode.indexOf('//') === 0 ? 'https:' : '';\n                    var params = { rel: 0, enablejsapi: 1, origin: window.location.origin };\n                    if (slideData.embedCode.indexOf(\"//drive.google.com\") === -1) {\n                        params.autoplay = 1;\n                    }\n                    slideData.embedUrl = slideData.embedCode ? scheme + slideData.embedCode + separator + $.param(params) : \"\";\n                } else if (slideData.category === 'video' && slideData.videoSourceType === 'vimeo') {\n                    slideData.embedCode = markup(slideData.embedCode);\n                } else if (slideData.category === 'infographic') {\n                    slideData.embedUrl = `/web/image/slide.slide/${encodeURIComponent(slideData.id)}/image_1024`;\n                } else if (slideData.category === 'document') {\n                    slideData.embedUrl = $(slideData.embedCode).attr('src');\n                }\n                // fill empty property to allow searching on it with list.filter(matcher)\n                slideData.isQuiz = !!slideData.isQuiz;\n                slideData.hasQuestion = !!slideData.hasQuestion;\n                // technical settings for the Fullscreen to work\n                var autoSetDone = false;\n                if (!slideData.hasQuestion) {\n                    if (['infographic', 'document', 'article'].includes(slideData.category)) {\n                        autoSetDone = true;  // images, documents (local + external) and articles are marked as completed when opened\n                    } else if (slideData.category === 'video' && slideData.videoSourceType === 'google_drive') {\n                        autoSetDone = true;  // google drive videos do not benefit from the YouTube integration and are marked as completed when opened\n                    }\n                }\n                slideData._autoSetDone = autoSetDone;\n            });\n            return slidesDataList;\n        },\n        /**\n         * Changes the url whenever the user changes slides.\n         * This allows the user to refresh the page and stay on the right slide\n         *\n         * @private\n         */\n        _pushUrlState: function () {\n            var urlParts = window.location.pathname.split('/');\n            urlParts[urlParts.length - 1] = this._slideValue.slug;\n            var url =  urlParts.join('/');\n            this.$('.o_wslides_fs_exit_fullscreen').attr('href', url);\n            var params = {'fullscreen': 1 };\n            if (this._slideValue.isQuiz) {\n                params.quiz = 1;\n            }\n            var fullscreenUrl = `${url}?${$.param(params)}`;\n            history.pushState(null, '', fullscreenUrl);\n        },\n        /**\n         * Render the current slide content using specific mecanism according to slide category:\n         * - simply append content (for article)\n         * - template rendering (for image, document, ....)\n         * - using a sub widget (quiz and video)\n         *\n         * @private\n         * @returns Deferred\n         */\n        _renderSlide: async function () {\n            // Avoid concurrent execution of the slide rendering as it writes the content at the same place anyway.\n            if (this._renderSlideRunning) { return; }\n            this._renderSlideRunning = true;\n            try {\n                const slide = this._slideValue;\n                var $content = this.$('.o_wslides_fs_content');\n                $content.empty();\n                if (this.websiteAnimateWidget) {\n                    this.websiteAnimateWidget.destroy()\n                    this.websiteAnimateWidget = null;\n                }\n\n                // display quiz slide, or quiz attached to a slide\n                if (slide.category === 'quiz' || slide.isQuiz) {\n                    $content.addClass('bg-white');\n                    var QuizWidget = new Quiz(this, slide, this.channel);\n                    return await QuizWidget.appendTo($content);\n                }\n\n                // render slide content\n                if (['document', 'infographic'].includes(slide.category)) {\n                    $content.empty().append(renderToElement('website.slides.fullscreen.content', {widget: this}));\n                } else if (slide.category === 'video' && slide.videoSourceType === 'youtube') {\n                    this.videoPlayer = new VideoPlayerYouTube(this, slide);\n                    return await this.videoPlayer.appendTo($content);\n                } else if (slide.category === 'video' && slide.videoSourceType === 'vimeo') {\n                    this.videoPlayer = new VideoPlayerVimeo(this, slide);\n                    return await this.videoPlayer.appendTo($content);\n                } else if (slide.category === 'video' && slide.videoSourceType === 'google_drive') {\n                    $content.empty().append(renderToElement('website.slides.fullscreen.video.google_drive', {widget: this}));\n                } else if (slide.category === 'article'){\n                    this.websiteAnimateWidget = new publicWidget.registry.WebsiteAnimate();\n                    var $wpContainer = $('<div>').addClass('o_wslide_fs_article_content bg-white block w-100 overflow-auto p-3');\n                    $wpContainer.html(slide.htmlContent);\n                    $content.append($wpContainer);\n                    this.trigger_up('widgets_start_request', {\n                        $target: $content,\n                    });\n                    this.websiteAnimateWidget.attachTo($wpContainer);\n                }\n                unhideConditionalElements();\n            } finally {\n                this._renderSlideRunning = false;\n            }\n        },\n        /**\n         * @private\n         */\n        _updateSlideValue: function (slide) {\n            if (this._slideValue === slide) {\n                return;\n            }\n            this._slideValue = slide;\n            this._onChangeSlide();\n        },\n\n        //--------------------------------------------------------------------------\n        // Handlers\n        //--------------------------------------------------------------------------\n        /**\n         * Triggered whenever the user changes slides.\n         * When the current slide is changed, widget will be automatically updated\n         * and allowed to: fetch the content if needed, render it, update the url,\n         * and set slide as \"completed\" according to its category requirements. In\n         * mobile case (i.e. limited screensize), sidebar will be toggled since\n         * sidebar will block most or all of new slide visibility.\n         *\n         * @private\n         */\n        _onChangeSlide: function () {\n            var self = this;\n            const slide = this._slideValue;\n            self._pushUrlState();\n            return this._fetchSlideContent().then(function() { // render content\n                var websiteName = document.title.split(\" | \")[1]; // get the website name from title\n                document.title =  (websiteName) ? slide.name + ' | ' + websiteName : slide.name;\n                if  (uiUtils.getSize() < SIZES.MD) {\n                    self._toggleSidebar(); // hide sidebar when small device screen\n                }\n                return self._renderSlide();\n            }).then(function() {\n                if (slide._autoSetDone && !session.is_website_user) {  // no useless RPC call\n                    if (slide.category === 'document') {\n                        // only set the slide as completed after iFrame is loaded to avoid concurrent execution with 'embedUrl' controller\n                        self.el.querySelector('iframe.o_wslides_iframe_viewer').addEventListener('load', () => self._toggleSlideCompleted(slide));\n                    } else {\n                           return self._toggleSlideCompleted(slide);\n                    }\n                }\n            });\n        },\n        /**\n         * Changes current slide when receiving custom event `change_slide` with\n         * its id and if it's its quizz or not we need to display.\n         *\n         * @private\n         */\n        _onChangeSlideRequest: function (ev) {\n            var slideData = ev.data;\n            var newSlide = findSlide(this.slides, {\n                id: slideData.id,\n                isQuiz: slideData.isQuiz || false,\n            });\n            this._updateSlideValue(newSlide);\n        },\n        /**\n         * After a slide has been marked as completed / uncompleted, update the state\n         * of this widget and reload the slide if needed (e.g. to re-show the questions\n         * of a quiz).\n         *\n         * We might need to set multiple slide as completed, because of \"isQuiz\"\n         * set to True / False\n         *\n         * @private\n         * @param {Object} slide: slide to set as completed\n         * @param {Boolean} completed: true to mark the slide as completed\n         *     false to mark the slide as not completed\n         */\n        _toggleSlideCompleted: async function (slide, completed = true) {\n            await this._super(...arguments);\n\n            const fsSlides = this.slides.filter(_slide => _slide.id === slide.id);\n\n            fsSlides.forEach(slide => slide.completed = completed);\n\n            const currentSlide = this._slideValue;\n            if (currentSlide.id === slide.id) {\n                currentSlide.completed = completed;\n                this._updateSlideValue(currentSlide);\n\n                if ((currentSlide.hasQuestion || currentSlide.type === 'quiz') && !completed) {\n                    // Reload the quiz\n                    this._renderSlide();\n                }\n            }\n        },\n        /**\n         * Go the next slide\n         *\n         * @private\n         */\n        _onSlideGoToNext: function (ev) {\n            this.sidebar.goNext();\n        },\n        /**\n         * Called when the sidebar toggle is clicked -> toggles the sidebar visibility.\n         *\n         * @private\n         */\n        _onClickToggleSidebar: function (ev){\n            ev.preventDefault();\n            this._toggleSidebar();\n        },\n\n        _onClickShareSlide: function (ev) {\n            const slide = this._slideValue;\n            this.call(\"dialog\", \"add\", SlideShareDialog, {\n                category: slide.category,\n                documentMaxPage: slide.category == 'document' && this.getDocumentMaxPage(),\n                emailSharing: slide.emailSharing === 'True',\n                embedCode: slide.embedCode || '',\n                id: slide.id,\n                isFullscreen: true,\n                name: slide.name,\n                url: slide.websiteShareUrl,\n            });\n        },\n\n        /**\n         * Toggles sidebar visibility.\n         *\n         * @private\n         */\n        _toggleSidebar: function () {\n            this.$('.o_wslides_fs_sidebar').toggleClass('o_wslides_fs_sidebar_hidden');\n            this.$('.o_wslides_fs_toggle_sidebar').toggleClass('active');\n        },\n    });\n\n    publicWidget.registry.websiteSlidesFullscreenPlayer = publicWidget.Widget.extend({\n        selector: '.o_wslides_fs_main',\n        start: function (){\n            var proms = [this._super.apply(this, arguments)];\n            var fullscreen = new Fullscreen(this, this._getSlides(), this._getCurrentSlideID(), this._extractChannelData());\n            proms.push(fullscreen.attachTo(\".o_wslides_fs_main\"));\n            // To prevent double scrollbar due to footer overflow\n            document.querySelector('.o_footer')?.classList.add('d-none');\n            return proms;\n        },\n        _extractChannelData: function (){\n            return this.$el.data();\n        },\n        _getCurrentSlideID: function (){\n            return parseInt(this.$('.o_wslides_fs_sidebar_list_item.active').data('id'));\n        },\n        /**\n         * @private\n         * Creates slides objects from every slide-list-cells attributes\n         */\n        _getSlides: function (){\n            var $slides = this.$('.o_wslides_fs_sidebar_list_item[data-can-access=\"True\"]');\n            var slideList = [];\n            $slides.each(function () {\n                var slideData = $(this).data();\n                slideList.push(slideData);\n            });\n            return slideList;\n        },\n    });\n\n    export default Fullscreen;\n", "/** @odoo-module **/\n\nimport { sprintf } from '@web/core/utils/strings';\nimport { renderToElement } from \"@web/core/utils/render\";\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nvar CourseJoinWidget = publicWidget.Widget.extend({\n    template: 'slide.course.join',\n    events: {\n        'click .o_wslides_js_course_join_link': '_onClickJoin',\n    },\n\n    /**\n     *\n     * Overridden to add options parameters.\n     *\n     * @param {Object} parent\n     * @param {Object} options\n     * @param {Object} options.channel slide.channel information\n     * @param {boolean} options.isMember whether current user is enrolled\n     * @param {boolean} options.isMemberOrInvited whether current user is at least invited\n     * @param {string} options.inviteHash hash of the invited attendee. Needed to grant\n     *   access to a course preview / to identify.\n     * @param {integer} options.invitePartnerId id of partner of invited attendee if any.\n     *   Also needed to access course preview / to identify.\n     * @param {boolean} options.invitePreview whether the course is rendered as a preview.\n     *   This is true when an invited attendee is on the course while unlogged.\n     * @param {boolean} options.isPartnerWithoutUser whether invited partner has users. Used\n     *   to redirect properly to sign up / log in.\n     * @param {string} [options.joinMessage] the message to use for the simple join case\n     *   when the course is free and the user is logged in, defaults to \"Join this Course\".\n     * @param {Promise} [options.beforeJoin] a promise to execute before we redirect to\n     *   another url within the join process (login / buy course / ...)\n     * @param {function} [options.afterJoin] a callback function called after the user has\n     *   joined the course\n     */\n    init: function (parent, options) {\n        this._super.apply(this, arguments);\n        this.channel = options.channel;\n        this.isMember = options.isMember;\n        this.isMemberOrInvited = options.isMemberOrInvited;\n        this.inviteHash = options.inviteHash;\n        this.invitePartnerId = options.invitePartnerId;\n        this.invitePreview = options.invitePreview;\n        this.isPartnerWithoutUser = options.isPartnerWithoutUser;\n        this.publicUser = options.publicUser;\n        this.joinMessage = options.joinMessage || _t('Join this Course');\n        this.beforeJoin = options.beforeJoin || function () {return Promise.resolve();};\n        this.afterJoin = options.afterJoin || function () {document.location.reload();};\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {MouseEvent} ev\n     */\n    _onClickJoin: function (ev) {\n        ev.preventDefault();\n\n        if (this.invitePreview || (this.channel.channelEnroll === 'invite' && this.isMemberOrInvited)) {\n            this.joinChannel(this.channel.channelId);\n            return;\n        }\n\n        if (this.channel.channelEnroll !== 'invite') {\n            if (this.publicUser) {\n                this.beforeJoin().then(this._redirectToLogin.bind(this));\n            } else if (!this.isMember) {\n                this.joinChannel(this.channel.channelId);\n            }\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Builds a login page that then redirects to this slide page, or the channel if the course\n     * is not configured as public enroll type.\n     *\n     * @private\n     */\n    _redirectToLogin: function () {\n        var url;\n        if (this.channel.channelEnroll === 'public') {\n            url = window.location.pathname;\n            if (document.location.href.indexOf(\"fullscreen\") !== -1) {\n                url += '?fullscreen=1';\n            }\n        } else {\n            url = `/slides/${encodeURIComponent(this.channel.channelId)}`;\n        }\n        document.location = sprintf('/web/login?redirect=%s', encodeURIComponent(url));\n    },\n\n    /**\n     * @private\n     * @param {Object} $el\n     * @param {String} message\n     */\n    _popoverAlert: function ($el, message) {\n        $el.popover({\n            trigger: 'focus',\n            delay: {'hide': 300},\n            placement: 'bottom',\n            container: 'body',\n            html: true,\n            content: function () {\n                return message;\n            }\n        }).popover('show');\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n    /**\n     * @public\n     * @param {integer} channelId\n     */\n    joinChannel: function (channelId) {\n        var self = this;\n        rpc('/slides/channel/join', {\n            channel_id: channelId,\n        }).then(function (data) {\n            if (!data.error) {\n                self.afterJoin();\n            } else {\n                if (data.error === 'public_user') {\n                    const popupContent = renderToElement('slide.course.join.popupContent', {\n                        channelId: channelId,\n                        courseUrl: encodeURIComponent(document.URL),\n                        errorSignupAllowed: data.error_signup_allowed,\n                        widget: self,\n                    });\n                    self._popoverAlert(self.$el, popupContent);\n                } else if (data.error === 'join_done') {\n                    self._popoverAlert(self.$el, _t('You have already joined this channel'));\n                } else {\n                    self._popoverAlert(self.$el, _t('Unknown error'));\n                }\n            }\n        });\n    },\n});\n\npublicWidget.registry.websiteSlidesCourseJoin = publicWidget.Widget.extend({\n    selector: '.o_wslides_js_course_join_link',\n\n    /**\n     * @override\n     * @param {Object} parent\n     */\n    start: function () {\n        var self = this;\n        var proms = [this._super.apply(this, arguments)];\n        var data = self.$el.data();\n        var options = {\n            channel: {\n                channelEnroll: data.channelEnroll,\n                channelId: data.channelId\n            },\n            inviteHash: data.inviteHash,\n            invitePartnerId: data.invitePartnerId,\n            invitePreview: data.invitePreview,\n            isMemberOrInvited: data.isMemberOrInvited,\n            isPartnerWithoutUser: data.isPartnerWithoutUser\n        };\n        $('.o_wslides_js_course_join').each(function () {\n            proms.push(new CourseJoinWidget(self, options).attachTo($(this)));\n        });\n        return Promise.all(proms);\n    },\n});\n\nexport default {\n    courseJoinWidget: CourseJoinWidget,\n    websiteSlidesCourseJoin: publicWidget.registry.websiteSlidesCourseJoin\n};\n", "/** @odoo-module **/\n\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { escape } from \"@web/core/utils/strings\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\nexport const WebsiteSlidesEnroll = publicWidget.Widget.extend({\n    selector: \"#wrapwrap\",\n    events: {\n        \"click .o_wslides_js_channel_enroll\": \"_onSendRequestClick\",\n    },\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n    },\n    async _onSendRequestClick(ev) {\n        ev.preventDefault();\n        const clickedEl = ev.currentTarget;\n        const channelId = parseInt(clickedEl.dataset.channelId);\n        await new Promise((resolve) =>\n            this.call(\"dialog\", \"add\", ConfirmationDialog, {\n                confirm: resolve,\n                title: _t(\"Request Access.\"),\n                body: _t(\"Do you want to request access to this course?\"),\n                confirmLabel: _t(\"Yes\"),\n                cancel: () => {}, // show cancel button\n            })\n        );\n        const { error, done } = await this.orm.call(\n            \"slide.channel\",\n            \"action_request_access\",\n            [channelId],\n        );\n        const $alert = $(clickedEl.closest(\".alert\"));\n        const message = done ? _t(\"Request sent!\") : error || _t(\"Unknown error, try again.\");\n        $alert.replaceWith(`\n            <div class=\"alert alert-${done ? \"success\" : \"danger\"}\" role=\"alert\">\n                <strong>${escape(message)}</strong>\n            </div>`);\n    },\n});\n\npublicWidget.registry.WebsiteSlidesEnroll = WebsiteSlidesEnroll;\n", "/** @odoo-module **/\n\nimport { renderToElement } from \"@web/core/utils/render\";\nimport publicWidget from '@web/legacy/js/public/public_widget';\n\npublicWidget.registry.websiteSlidesCoursePrerequisite = publicWidget.Widget.extend({\n    selector: '.o_wslides_js_prerequisite_course',\n\n    async start() {\n        await this._super(...arguments);\n        const channels = this.$el.data('channels');\n        this.$el.popover({\n            trigger: 'focus',\n            placement: 'bottom',\n            container: 'body',\n            html: true,\n            content: renderToElement('slide.course.prerequisite', {channels: channels}),\n        });\n    },\n});\n\nexport default {\n    websiteSlidesCoursePrerequisite: publicWidget.registry.websiteSlidesCoursePrerequisite\n};\n", "/** @odoo-module **/\n\n    import publicWidget from '@web/legacy/js/public/public_widget';\n    import { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n    import { renderToElement } from \"@web/core/utils/render\";\n    import { escape } from \"@web/core/utils/strings\";\n    import { session } from \"@web/session\";\n    import CourseJoin from '@website_slides/js/slides_course_join';\n    import QuestionFormWidget from '@website_slides/js/slides_course_quiz_question_form';\n    import { SlideCoursePage } from '@website_slides/js/slides_course_page';\n    import { rpc } from \"@web/core/network/rpc\";\n    import { SlideQuizFinishDialog } from \"@website_slides/js/public/components/slide_quiz_finish_dialog/slide_quiz_finish_dialog\";\n    import { user } from \"@web/core/user\";\n\n    import { _t } from \"@web/core/l10n/translation\";\n\n    import { markup } from \"@odoo/owl\";\n\n    const CourseJoinWidget = CourseJoin.courseJoinWidget;\n\n    /**\n     * This widget is responsible of displaying quiz questions and propositions. Submitting the quiz will fetch the\n     * correction and decorate the answers according to the result. Error message or modal can be displayed.\n     *\n     * This widget can be attached to DOM rendered server-side by `website_slides.slide_category_quiz` or\n     * used client side (Fullscreen).\n     *\n     * Triggered events are :\n     * - slide_go_next: need to go to the next slide, when quiz is done. Event data contains the current slide id.\n     * - quiz_completed: when the quiz is passed and completed by the user. Event data contains current slide data.\n     */\n    var Quiz = publicWidget.Widget.extend({\n        template: 'slide.slide.quiz',\n        events: {\n            \"click .o_wslides_quiz_answer\": '_onAnswerClick',\n            \"click .o_wslides_js_lesson_quiz_submit\": '_submitQuiz',\n            \"click .o_wslides_quiz_continue\": '_onClickNext',\n            \"click .o_wslides_js_lesson_quiz_reset\": '_onClickReset',\n            'click .o_wslides_js_quiz_add': '_onCreateQuizClick',\n            'click .o_wslides_js_quiz_edit_question': '_onEditQuestionClick',\n            'click .o_wslides_js_quiz_delete_question': '_onDeleteQuestionClick',\n        },\n\n        custom_events: {\n            display_created_question: '_displayCreatedQuestion',\n            display_updated_question: '_displayUpdatedQuestion',\n            reset_display: '_resetDisplay',\n            delete_question: '_deleteQuestion',\n        },\n\n        /**\n        * @override\n        * @param {Object} parent\n        * @param {Object} slide_data holding all the classic slide information\n        * @param {Object} quiz_data : optional quiz data to display. If not given, will be fetched. (questions and answers).\n        */\n        init: function (parent, slide_data, channel_data, quiz_data) {\n            this._super.apply(this, arguments);\n            this.slide = Object.assign({\n                id: 0,\n                name: '',\n                hasNext: false,\n                completed: false,\n                isMember: false,\n                isMemberOrInvited: false,\n            }, slide_data);\n            this.quiz = quiz_data || false;\n            if (this.quiz) {\n                this.quiz.questionsCount = quiz_data.questions.length;\n            }\n            this.isMember = slide_data.isMember || false;\n            this.isMemberOrInvited = slide_data.isMemberOrInvited || false;\n            this.publicUser = session.is_website_user;\n            this.userId = user.userId;\n            this.redirectURL = encodeURIComponent(document.URL);\n            this.channel = channel_data;\n\n            this.orm = this.bindService(\"orm\");\n        },\n\n        /**\n         * @override\n         */\n        willStart: function () {\n            var defs = [this._super.apply(this, arguments)];\n            if (!this.quiz) {\n                defs.push(this._fetchQuiz());\n            }\n            return Promise.all(defs);\n        },\n\n        /**\n         * Overridden to add custom rendering behavior upon start of the widget.\n         *\n         * If the user has answered the quiz before having joined the course, we check\n         * their answers (saved into their session) here as well.\n         *\n         * @override\n         */\n        start: function () {\n            var self = this;\n            return this._super.apply(this, arguments).then(function ()  {\n                self._renderValidationInfo();\n                self._bindSortable();\n                self._checkLocationHref();\n                if (!self.isMember) {\n                    self._renderJoinWidget();\n                } else if (self.slide.sessionAnswers) {\n                    self._applySessionAnswers();\n                    self._submitQuiz();\n                }\n            });\n        },\n\n        destroy() {\n            this._unbindSortable();\n            return this._super(...arguments);\n        },\n\n        //--------------------------------------------------------------------------\n        // Private\n        //--------------------------------------------------------------------------\n\n        _showErrorMessage: function (errorCode) {\n            var message = _t('There was an error validating this quiz.');\n            if (errorCode === 'slide_quiz_incomplete') {\n                message = _t('All questions must be answered!');\n            } else if (errorCode === 'slide_quiz_done') {\n                message = _t('This quiz is already done. Retaking it is not possible.');\n            } else if (errorCode === 'public_user') {\n                message = _t('You must be logged to submit the quiz.');\n            }\n\n            this.$('.o_wslides_js_quiz_submit_error')\n                .removeClass('d-none')\n                .find('.o_wslides_js_quiz_submit_error_text')\n                .text(message);\n        },\n\n        _hideErrorMessage: function () {\n            this.$('.o_wslides_js_quiz_submit_error')\n                .addClass('d-none');\n        },\n\n        /**\n         * Allows to reorder the questions\n         * @private\n         */\n        _bindSortable: function () {\n            this.bindedSortable = this.call(\n                \"sortable\",\n                \"create\",\n                {\n                    ref: { el: this.el },\n                    handle: \".o_wslides_js_quiz_sequence_handler\",\n                    elements: \".o_wslides_js_lesson_quiz_question\",\n                    onDrop: this._reorderQuestions.bind(this),\n                    clone: false,\n                    placeholderClasses: ['o_wslides_js_quiz_sequence_highlight', 'position-relative', 'my-3'],\n                    applyChangeOnDrop: true\n                },\n            ).enable();\n        },\n\n        _unbindSortable: function () {\n            this.bindedSortable?.cleanup();\n        },\n\n        /**\n         * Get all the questions ID from the displayed Quiz\n         * @returns {Array}\n         * @private\n         */\n        _getQuestionsIds: function () {\n            return this.$('.o_wslides_js_lesson_quiz_question').map(function () {\n                return $(this).data('question-id');\n            }).get();\n        },\n\n        /**\n         * Modify visually the sequence of all the questions after\n         * calling the _reorderQuestions RPC call.\n         * @private\n         */\n        _modifyQuestionsSequence: function () {\n            this.$('.o_wslides_js_lesson_quiz_question').each(function (index, question) {\n                $(question).find('span.o_wslides_quiz_question_sequence').text(index + 1);\n            });\n        },\n\n        /**\n         * RPC call to resequence all the questions. It is called\n         * after modifying the sequence of a question and also after\n         * deleting a question.\n         * @private\n         */\n        _reorderQuestions: function () {\n            rpc('/web/dataset/resequence', {\n                model: \"slide.question\",\n                ids: this._getQuestionsIds()\n            }).then(this._modifyQuestionsSequence.bind(this))\n        },\n        /*\n         * @private\n         * Fetch the quiz for a particular slide\n         */\n        _fetchQuiz: function () {\n            var self = this;\n            return rpc('/slides/slide/quiz/get', {\n                'slide_id': self.slide.id,\n            }).then(function (quiz_data) {\n                self.slide.sessionAnswers = quiz_data.session_answers;\n                self.quiz = {\n                    description_safe: quiz_data.slide_description ? markup(quiz_data.slide_description) : '',\n                    questions: quiz_data.slide_questions || [],\n                    questionsCount: quiz_data.slide_questions.length,\n                    quizAttemptsCount: quiz_data.quiz_attempts_count || 0,\n                    quizKarmaGain: quiz_data.quiz_karma_gain || 0,\n                    quizKarmaWon: quiz_data.quiz_karma_won || 0,\n                    slideResources: quiz_data.slide_resource_ids || [],\n                };\n            });\n        },\n\n        /**\n         * Hide the edit and delete button and also the handler\n         * to resequence the question\n         * @private\n         */\n        _hideEditOptions: function () {\n            this.$('.o_wslides_js_lesson_quiz_question .o_wslides_js_quiz_edit_del,' +\n                   ' .o_wslides_js_lesson_quiz_question .o_wslides_js_quiz_sequence_handler').addClass('d-none');\n        },\n\n        /**\n         * @private\n         * Decorate the answers according to state\n         */\n        _disableAnswers: function () {\n            var self = this;\n            this.$('.o_wslides_js_lesson_quiz_question').addClass('completed-disabled');\n            this.$('input[type=radio]').each(function () {\n                $(this).prop('disabled', self.slide.completed);\n            });\n        },\n\n        /**\n         * Decorate the answer inputs according to the correction and adds the answer comment if\n         * any.\n         *\n         * @private\n         */\n        _renderAnswersHighlightingAndComments: function () {\n            var self = this;\n            this.$('.o_wslides_js_lesson_quiz_question').each(function () {\n                var $question = $(this);\n                var questionId = $question.data('questionId');\n                var isCorrect = self.quiz.answers[questionId].is_correct;\n                $question.find('a.o_wslides_quiz_answer').each(function () {\n                    var $answer = $(this);\n                    $answer.find('i.fa').addClass('d-none');\n                    if ($answer.find('input[type=radio]')[0].checked) {\n                        if (isCorrect) {\n                            $answer.removeClass('list-group-item-danger').addClass('list-group-item-success');\n                            $answer.find('i.fa-check-circle').removeClass('d-none');\n                        } else {\n                            $answer.removeClass('list-group-item-success').addClass('list-group-item-danger');\n                            $answer.find('i.fa-times-circle').removeClass('d-none');\n                            $answer.find('label input').prop('checked', false);\n                        }\n                    } else {\n                        $answer.removeClass('list-group-item-danger list-group-item-success');\n                        $answer.find('i.fa-circle').removeClass('d-none');\n                    }\n                });\n                var comment = self.quiz.answers[questionId].comment;\n                if (comment) {\n                    $question.find('.o_wslides_quiz_answer_info').removeClass('d-none');\n                    $question.find('.o_wslides_quiz_answer_comment').text(comment);\n                }\n            });\n        },\n\n        /**\n         * Will check if we have answers coming from the session and re-apply them.\n         */\n        _applySessionAnswers: function () {\n            if (!this.slide.sessionAnswers || this.slide.sessionAnswers.length === 0) {\n                return;\n            }\n\n            var self = this;\n            this.$('.o_wslides_js_lesson_quiz_question').each(function () {\n                var $question = $(this);\n                $question.find('a.o_wslides_quiz_answer').each(function () {\n                    var $answer = $(this);\n                    if (!$answer.find('input[type=radio]')[0].checked &&\n                        self.slide.sessionAnswers.includes($answer.data('answerId'))) {\n                        $answer.find('input[type=radio]').prop('checked', true);\n                    }\n                });\n            });\n\n            // reset answers coming from the session\n            this.slide.sessionAnswers = false;\n        },\n\n        /*\n         * @private\n         * Update validation box (karma, buttons) according to widget state\n         */\n        _renderValidationInfo: function () {\n            var $validationElem = this.$('.o_wslides_js_lesson_quiz_validation');\n            $validationElem.empty().append(\n                renderToElement('slide.slide.quiz.validation', {'widget': this})\n            );\n        },\n        /*\n        * Toggle additional resource info box\n        *\n        * @private\n        * @param {Boolean} show - Whether show or hide the information\n        */\n        _toggleAdditionalResourceInfo: function(show) {\n            const resourceInfo = document.getElementsByClassName('o_wslides_js_lesson_quiz_resource_info')[0];\n            resourceInfo && (show ? resourceInfo.classList.remove('d-none') : resourceInfo.classList.add('d-none'));\n        },\n        /**\n         * Renders the button to join a course.\n         * If the user is logged in, the course is public, and the user has previously tried to\n         * submit answers, we automatically attempt to join the course.\n         *\n         * @private\n         */\n        _renderJoinWidget: function () {\n            var $widgetLocation = this.$(\".o_wslides_join_course_widget\");\n            if ($widgetLocation.length !== 0) {\n                var courseJoinWidget = new CourseJoinWidget(this, {\n                    isQuiz: true,\n                    channel: this.channel,\n                    isMember: this.isMember,\n                    isMemberOrInvited: this.isMemberOrInvited,\n                    publicUser: this.publicUser,\n                    beforeJoin: this._saveQuizAnswersToSession.bind(this),\n                    afterJoin: this._afterJoin.bind(this),\n                    joinMessage: _t('Join & Submit'),\n                });\n\n                courseJoinWidget.appendTo($widgetLocation);\n                if (!this.publicUser && courseJoinWidget.channel.channelEnroll === 'public' && this.slide.sessionAnswers) {\n                    courseJoinWidget.joinChannel(this.channel.channelId);\n                }\n            }\n        },\n\n        /**\n         * Get the quiz answers filled in by the User\n         *\n         * @private\n         */\n        _getQuizAnswers: function () {\n            return this.$('input[type=radio]:checked').map(function (index, element) {\n                return parseInt($(element).val());\n            }).get();\n        },\n\n        /**\n         * Submit a quiz and get the correction. It will display messages\n         * according to quiz result.\n         *\n         * @private\n         */\n         async _submitQuiz() {\n            const data = await rpc('/slides/slide/quiz/submit', {\n                slide_id: this.slide.id,\n                answer_ids: this._getQuizAnswers(),\n            });\n            if (data.error) {\n                this._showErrorMessage(data.error);\n                return;\n            } else {\n                this._hideErrorMessage();\n            }\n            Object.assign(this.quiz, data);\n            const {rankProgress, completed, channel_completion: completion} = this.quiz;\n            // three of the rankProgress properties are HTML messages, mark if set\n            if ('description' in rankProgress) {\n                rankProgress['description'] = markup(rankProgress['description'] || '');\n                rankProgress['previous_rank']['motivational'] =\n                    markup(rankProgress['previous_rank']['motivational'] || '');\n                rankProgress['new_rank']['motivational'] =\n                    markup(rankProgress['new_rank']['motivational'] || '');\n            }\n            if (completed) {\n                this._disableAnswers();\n                this.call(\"dialog\", \"add\", SlideQuizFinishDialog, {\n                    quiz: this.quiz,\n                    hasNext: this.slide.hasNext,\n                    onClickNext: (ev) => this._onClickNext(ev),\n                    userId: this.userId,\n                });\n                this.slide.completed = true;\n                this.trigger_up('slide_completed', {\n                    slideId: this.slide.id,\n                    channelCompletion: completion,\n                    completed: true,\n                });\n            }\n            this._hideEditOptions();\n            this._renderAnswersHighlightingAndComments();\n            this._renderValidationInfo();\n            this._toggleAdditionalResourceInfo(!completed);\n        },\n\n        /**\n         * Get all the question information after clicking on\n         * the edit button\n         * @param $elem\n         * @returns {{id: *, sequence: number, text: *, answers: Array}}\n         * @private\n         */\n        _getQuestionDetails: function ($elem) {\n            var answers = [];\n            $elem.find('.o_wslides_quiz_answer').each(function () {\n                answers.push({\n                    'id': $(this).data('answerId'),\n                    'text_value': $(this).data('text'),\n                    'is_correct': $(this).data('isCorrect'),\n                    'comment': $(this).data('comment')\n                });\n            });\n            return {\n                'id': $elem.data('questionId'),\n                'sequence': parseInt($elem.find('.o_wslides_quiz_question_sequence').text()),\n                'text': $elem.data('title'),\n                'answers': answers,\n            };\n        },\n\n        /**\n         * If the slides has been called with the Add Quiz button on the slide list\n         * it goes straight to the 'Add Quiz' button and clicks on it.\n         * @private\n         */\n        _checkLocationHref: function () {\n            if (window.location.href.includes('quiz_quick_create') && this.quiz.questionsCount === 0) {\n                this._onCreateQuizClick();\n            }\n        },\n\n        //--------------------------------------------------------------------------\n        // Handlers\n        //--------------------------------------------------------------------------\n\n        /**\n         * When clicking on an answer, this one should be marked as \"checked\".\n         *\n         * @private\n         * @param OdooEvent ev\n         */\n        _onAnswerClick: function (ev) {\n            ev.preventDefault();\n            if (!this.slide.completed) {\n                $(ev.currentTarget).find('input[type=radio]').prop('checked', true);\n            }\n        },\n\n        /**\n         * Triggering a event to switch to next slide\n         *\n         * @private\n         * @param OdooEvent ev\n         */\n        _onClickNext: function (ev) {\n            if (this.slide.hasNext) {\n                this.trigger_up('slide_go_next');\n            }\n        },\n\n        /**\n         * Resets the completion of the slide so the user can take\n         * the quiz again\n         *\n         * @private\n         */\n        _onClickReset: function () {\n            rpc('/slides/slide/quiz/reset', {\n                slide_id: this.slide.id\n            }).then(function () {\n                window.location.reload();\n            });\n        },\n        /**\n         * Saves the answers from the user and redirect the user to the\n         * specified url\n         *\n         * @private\n         */\n        _saveQuizAnswersToSession: function () {\n            this._hideErrorMessage();\n\n            return rpc('/slides/slide/quiz/save_to_session', {\n                'quiz_answers': {'slide_id': this.slide.id, 'slide_answers': this._getQuizAnswers()},\n            });\n        },\n        /**\n        * After joining the course, we save the questions in the session\n        * and reload the page to update the view.\n        *\n        * @private\n        */\n       _afterJoin: function () {\n            this._saveQuizAnswersToSession().then(() => {\n                window.location.reload();\n            });\n       },\n\n        /**\n         * When clicking on 'Add a Question' or 'Add Quiz' it\n         * initialize a new QuestionFormWidget to input the new\n         * question.\n         * @private\n         */\n        _onCreateQuizClick: function () {\n            var $elem = this.$('.o_wslides_js_lesson_quiz_new_question');\n            this.$('.o_wslides_js_quiz_add').addClass('d-none');\n            new QuestionFormWidget(this, {\n                slideId: this.slide.id,\n                sequence: this.quiz.questionsCount + 1\n            }).appendTo($elem);\n        },\n\n        /**\n         * When clicking on the edit button of a question it\n         * initialize a new QuestionFormWidget with the existing\n         * question as inputs.\n         * @param ev\n         * @private\n         */\n        _onEditQuestionClick: function (ev) {\n            var $editedQuestion = $(ev.currentTarget).closest('.o_wslides_js_lesson_quiz_question');\n            var question = this._getQuestionDetails($editedQuestion);\n            new QuestionFormWidget(this, {\n                editedQuestion: $editedQuestion,\n                question: question,\n                slideId: this.slide.id,\n                sequence: question.sequence,\n                update: true\n            }).insertAfter($editedQuestion);\n            $editedQuestion.hide();\n        },\n\n        /**\n         * When clicking on the delete button of a question it toggles a modal\n         * to confirm the deletion. When confirming it sends an RPC request to\n         * delete the Question and triggers an event to delete it from the UI.\n         * @param ev\n         * @private\n         */\n        _onDeleteQuestionClick: function (ev) {\n            const question = ev.currentTarget.closest('.o_wslides_js_lesson_quiz_question');\n            const questionId = parseInt(question.dataset.questionId);\n            this.call('dialog', 'add', ConfirmationDialog, {\n                title: _t('Delete Question'),\n                body: markup(_t('Are you sure you want to delete this question \"<strong>%s</strong>\"?', escape(question.dataset.title))),\n                cancel: () => {\n                },\n                cancelLabel: _t('No'),\n                confirm: async () => {\n                    await this.orm.unlink('slide.question', [questionId]);\n                    this.trigger_up('delete_question', { questionId });\n                },\n                confirmLabel: _t('Yes'),\n            });\n        },\n\n        /**\n         * Displays the created Question at the correct place (after the last question or\n         * at the first place if there is no questions yet) It also displays the 'Add Question'\n         * button or open a new QuestionFormWidget if the user wants to immediately add another one.\n         *\n         * @param event\n         * @private\n         */\n        _displayCreatedQuestion: function (event) {\n            var $lastQuestion = this.$('.o_wslides_js_lesson_quiz_question:last');\n            if ($lastQuestion.length !== 0) {\n                $lastQuestion.after(event.data.newQuestionRenderedTemplate);\n            } else {\n                this.$el.prepend(event.data.newQuestionRenderedTemplate);\n            }\n            this.quiz.questionsCount++;\n            event.data.questionFormWidget.destroy();\n            this.$('.o_wslides_js_quiz_add_question').removeClass('d-none');\n        },\n\n        /**\n         * Replace the edited question by the new question and destroy\n         * the QuestionFormWidget.\n         * @param event\n         * @private\n         */\n        _displayUpdatedQuestion: function (event) {\n            var questionFormWidget = event.data.questionFormWidget;\n            event.data.$editedQuestion.replaceWith(event.data.newQuestionRenderedTemplate);\n            questionFormWidget.destroy();\n        },\n\n        /**\n         * If the user cancels the creation or update of a Question it resets the display\n         * of the updated Question or it displays back the buttons.\n         *\n         * @param event\n         * @private\n         */\n        _resetDisplay: function (event) {\n            var questionFormWidget = event.data.questionFormWidget;\n            if (questionFormWidget.update) {\n                questionFormWidget.$editedQuestion.show();\n            } else {\n                if (this.quiz.questionsCount > 0) {\n                    this.$('.o_wslides_js_quiz_add_question').removeClass('d-none');\n                } else {\n                    this.$('.o_wslides_js_quiz_add_quiz').removeClass('d-none');\n                }\n            }\n            questionFormWidget.destroy();\n        },\n\n        /**\n         * After deletion of a Question the display is refreshed with the removal of the Question\n         * the reordering of all the remaining Questions and the change of the new Question sequence\n         * if the QuestionFormWidget is initialized.\n         *\n         * @param event\n         * @private\n         */\n        _deleteQuestion: function (event) {\n            var questionId = event.data.questionId;\n            this.$('.o_wslides_js_lesson_quiz_question[data-question-id=' + questionId + ']').remove();\n            this.quiz.questionsCount--;\n            this._reorderQuestions();\n            var $newQuestionSequence = this.$('.o_wslides_js_lesson_quiz_new_question .o_wslides_quiz_question_sequence');\n            $newQuestionSequence.text(parseInt($newQuestionSequence.text()) - 1);\n            if (this.quiz.questionsCount === 0 && !this.$('.o_wsildes_quiz_question_input').length) {\n                this.$('.o_wslides_js_quiz_add_quiz').removeClass('d-none');\n                this.$('.o_wslides_js_quiz_add_question').addClass('d-none');\n                this.$('.o_wslides_js_lesson_quiz_validation').addClass('d-none');\n            }\n        },\n    });\n\n    publicWidget.registry.websiteSlidesQuizNoFullscreen = SlideCoursePage.extend({\n        selector: '.o_wslides_lesson_main', // selector of complete page, as we need slide content and aside content table\n        custom_events: Object.assign({}, SlideCoursePage.prototype.custom_events, {\n            slide_go_next: '_onQuizNextSlide',\n        }),\n\n        //----------------------------------------------------------------------\n        // Public\n        //----------------------------------------------------------------------\n\n        /**\n         * @override\n         * @param {Object} parent\n         */\n        start: function () {\n            const ret = this._super(...arguments);\n\n            const $quiz = this.$('.o_wslides_js_lesson_quiz');\n            if ($quiz.length) {\n                const slideData = $quiz.data();\n                const channelData = this._extractChannelData(slideData);\n                slideData.quizData = {\n                    questions: this._extractQuestionsAndAnswers(),\n                    sessionAnswers: slideData.sessionAnswers || [],\n                    quizKarmaMax: slideData.quizKarmaMax,\n                    quizKarmaWon: slideData.quizKarmaWon || 0,\n                    quizKarmaGain: slideData.quizKarmaGain,\n                    quizAttemptsCount: slideData.quizAttemptsCount,\n                };\n\n                this.quiz = new Quiz(this, slideData, channelData, slideData.quizData);\n                this.quiz.attachTo($quiz);\n            } else {\n                this.quiz = null;\n            }\n            return ret;\n        },\n\n        //----------------------------------------------------------------------\n        // Handlers\n        //---------------------------------------------------------------------\n        _onQuizNextSlide: function () {\n            var url = this.$('.o_wslides_js_lesson_quiz').data('next-slide-url');\n            window.location.replace(url);\n        },\n\n        //----------------------------------------------------------------------\n        // Private\n        //---------------------------------------------------------------------\n\n        /**\n         * Get the slide data from the elements in the DOM.\n         *\n         * We need this overwrite because a documentation in non-fullscreen view\n         * doesn't have the standard \"done\" button and so in that case the slide\n         * data can not be retrieved.\n         *\n         * @override\n         * @param {Integer} slideId\n         */\n        _getSlide: function (slideId) {\n            const slide = this._super(...arguments);\n            if (slide) {\n                return slide;\n            }\n            // A quiz in a documentation on non fullscreen view\n            return $(`.o_wslides_js_lesson_quiz[data-id=\"${slideId}\"]`).data();\n        },\n\n        /**\n         * After a slide has been marked as completed / uncompleted, update the state\n         * of this widget and reload the slide if needed (e.g. to re-show the questions\n         * of a quiz).\n         *\n         * @override\n         * @param {Object} slide\n         * @param {Boolean} completed\n         */\n        toggleCompletionButton: function (slide, completed = true) {\n            this._super(...arguments);\n\n            if (this.quiz && this.quiz.slide.id === slide.id && !completed && this.quiz.quiz.questionsCount) {\n                // The quiz has been marked as \"Not Done\", re-load the questions\n                this.quiz.quiz.answers = null;\n                this.quiz.quiz.sessionAnswers = null;\n                this.quiz.slide.completed = false;\n                this.quiz._fetchQuiz().then(() => {\n                    this.quiz.renderElement();\n                    this.quiz._renderValidationInfo();\n                });\n\n            }\n\n            // The quiz has been submitted in a documentation and in non fullscreen view,\n            // should update the button \"Mark Done\" to \"Mark To Do\"\n            const $doneButton = $('.o_wslides_done_button');\n            if ($doneButton.length && completed) {\n                $doneButton\n                    .removeClass('o_wslides_done_button disabled btn-primary text-white')\n                    .addClass('o_wslides_undone_button btn-light')\n                    .text(_t('Mark To Do'))\n                    .removeAttr('title')\n                    .removeAttr('aria-disabled')\n                    .attr('href', `/slides/slide/${encodeURIComponent(slide.id)}/set_uncompleted`);\n            }\n        },\n\n        _extractChannelData: function (slideData) {\n            return {\n                channelId: slideData.channelId,\n                channelEnroll: slideData.channelEnroll,\n                channelRequestedAccess: slideData.channelRequestedAccess || false,\n                signupAllowed: slideData.signupAllowed\n            };\n        },\n\n        /**\n         * Extract data from exiting DOM rendered server-side, to have the list of questions with their\n         * relative answers.\n         * This method should return the same format as /slide/quiz/get controller.\n         *\n         * @return {Array<Object>} list of questions with answers\n         */\n        _extractQuestionsAndAnswers: function () {\n            var questions = [];\n            this.$('.o_wslides_js_lesson_quiz_question').each(function () {\n                var $question = $(this);\n                var answers = [];\n                $question.find('.o_wslides_quiz_answer').each(function () {\n                    var $answer = $(this);\n                    answers.push({\n                        id: $answer.data('answerId'),\n                        text: $answer.data('text'),\n                    });\n                });\n                questions.push({\n                    id: $question.data('questionId'),\n                    title: $question.data('title'),\n                    answer_ids: answers,\n                });\n            });\n            return questions;\n        },\n    });\n\n    export var Quiz = Quiz;\n    export const websiteSlidesQuizNoFullscreen = publicWidget.registry.websiteSlidesQuizNoFullscreen;\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { rpc } from \"@web/core/network/rpc\";\n\n/**\n * This Widget is responsible of displaying the question inputs when adding a new question or when updating an\n * existing one. When validating the question it makes an RPC call to the server and trigger an event for\n * displaying the question by the Quiz widget.\n */\nvar QuestionFormWidget = publicWidget.Widget.extend({\n    template: 'slide.quiz.question.input',\n    events: {\n        'click .o_wslides_js_quiz_validate_question': '_validateQuestion',\n        'click .o_wslides_js_quiz_cancel_question': '_cancelValidation',\n        'click .o_wslides_js_quiz_comment_answer': '_toggleAnswerLineComment',\n        'click .o_wslides_js_quiz_add_answer': '_addAnswerLine',\n        'click .o_wslides_js_quiz_remove_answer': '_removeAnswerLine',\n        'click .o_wslides_js_quiz_remove_answer_comment': '_removeAnswerLineComment',\n        'change .o_wslides_js_quiz_answer_comment > input[type=text]': '_onCommentChanged'\n    },\n\n    /**\n     * @override\n     * @param parent\n     * @param options\n     */\n    init: function (parent, options) {\n        this.$editedQuestion = options.editedQuestion;\n        this.question = options.question || {};\n        this.update = options.update;\n        this.sequence = options.sequence;\n        this.slideId = options.slideId;\n        this._super.apply(this, arguments);\n    },\n\n    /**\n     * @override\n     * @returns {*}\n     */\n    start: function () {\n        var self = this;\n        return this._super.apply(this, arguments).then(function () {\n            self.$('.o_wslides_quiz_question input').focus();\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     *\n     * @param commentInput\n     * @private\n     */\n    _onCommentChanged: function (event) {\n        var input = event.currentTarget;\n        var commentIcon = $(input).closest('.o_wslides_js_quiz_answer').find('.o_wslides_js_quiz_comment_answer');\n        if (input.value.trim() !== '') {\n            commentIcon.addClass('text-primary');\n            commentIcon.removeClass('text-muted');\n        } else {\n            commentIcon.addClass('text-muted');\n            commentIcon.removeClass('text-primary');\n        }\n    },\n\n    /**\n     * Toggle the input for commenting the answer line which will be\n     * seen by the frontend user when submitting the quiz.\n     * @param ev\n     * @private\n     */\n    _toggleAnswerLineComment: function (ev) {\n        var commentLine = $(ev.currentTarget).closest('.o_wslides_js_quiz_answer').find('.o_wslides_js_quiz_answer_comment').toggleClass('d-none');\n        commentLine.find('input[type=text]').focus();\n    },\n\n    /**\n     * Adds a new answer line after the element the user clicked on\n     * e.g. If there is 3 answer lines and the user click on the add\n     *      answer button on the second line, the new answer line will\n     *      display between the second and the third line.\n     * @param ev\n     * @private\n     */\n    _addAnswerLine: function (ev) {\n        $(ev.currentTarget).closest('.o_wslides_js_quiz_answer').after(renderToElement('slide.quiz.answer.line'));\n    },\n\n    /**\n     * Removes an answer line. Can't remove the last answer line.\n     * @param ev\n     * @private\n     */\n    _removeAnswerLine: function (ev) {\n        if (this.$('.o_wslides_js_quiz_answer').length > 1) {\n            $(ev.currentTarget).closest('.o_wslides_js_quiz_answer').remove();\n        }\n    },\n\n    /**\n     *\n     * @param ev\n     * @private\n     */\n    _removeAnswerLineComment: function (ev) {\n        var commentLine = $(ev.currentTarget).closest('.o_wslides_js_quiz_answer_comment').addClass('d-none');\n        commentLine.find('input[type=text]').val('').change();\n    },\n\n    /**\n     * Handler when user click on 'Save' or 'Update' buttons.\n     * @param ev\n     * @private\n     */\n    _validateQuestion: function (ev) {\n        this._createOrUpdateQuestion({\n            update: $(ev.currentTarget).hasClass('o_wslides_js_quiz_update'),\n        });\n    },\n\n    /**\n     * Handler when user click on the 'Cancel' button.\n     * Calls a method from slides_course_quiz.js widget\n     * which will handle the reset of the question display.\n     * @private\n     */\n    _cancelValidation: function () {\n        this.trigger_up('reset_display', {\n            questionFormWidget: this,\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * RPC call to create or update a question.\n     * Triggers method from slides_course_quiz.js to\n     * correctly display the question.\n     * @param options\n     * @private\n     */\n    _createOrUpdateQuestion: async function (options) {\n        var $form = this.$('form');\n\n        if (this._isValidForm($form)) {\n            var values = this._serializeForm($form);\n            var renderedQuestion = await rpc('/slides/slide/quiz/question_add_or_update', values);\n\n            if (typeof renderedQuestion === 'object' && renderedQuestion.error) {\n                this.$('.o_wslides_js_quiz_validation_error')\n                    .removeClass('d-none')\n                    .find('.o_wslides_js_quiz_validation_error_text')\n                    .text(renderedQuestion.error);\n            } else if (options.update) {\n                this.$('.o_wslides_js_quiz_validation_error').addClass('d-none');\n                this.trigger_up('display_updated_question', {\n                    newQuestionRenderedTemplate: renderedQuestion,\n                    $editedQuestion: this.$editedQuestion,\n                    questionFormWidget: this,\n                });\n            } else {\n                this.$('.o_wslides_js_quiz_validation_error').addClass('d-none');\n                this.trigger_up('display_created_question', {\n                    newQuestionRenderedTemplate: renderedQuestion,\n                    questionFormWidget: this\n                });\n            }\n        } else {\n            this.$('.o_wslides_js_quiz_validation_error')\n                .removeClass('d-none')\n                .find('.o_wslides_js_quiz_validation_error_text')\n                .text(_t('Please fill in the question'));\n            this.$('.o_wslides_quiz_question input').focus();\n        }\n    },\n\n    /**\n     * Check if the Question has been filled up\n     * @param $form\n     * @returns {boolean}\n     * @private\n     */\n    _isValidForm: function($form) {\n        return $form.find('.o_wslides_quiz_question input[type=text]').val().trim() !== \"\";\n    },\n\n    /**\n     * Serialize the form into a JSON object to send it\n     * to the server through a RPC call.\n     * @param $form\n     * @returns {{id: *, sequence: *, question: *, slide_id: *, answer_ids: Array}}\n     * @private\n     */\n    _serializeForm: function ($form) {\n        var answers = [];\n        var sequence = 1;\n        $form.find('.o_wslides_js_quiz_answer').each(function () {\n            var value = $(this).find('.o_wslides_js_quiz_answer_value').val();\n            if (value.trim() !== \"\") {\n                var answer = {\n                    'sequence': sequence++,\n                    'text_value': value,\n                    'is_correct': $(this).find('input[type=radio]').prop('checked') === true,\n                    'comment': $(this).find('.o_wslides_js_quiz_answer_comment input[type=text]').val().trim()\n                };\n                answers.push(answer);\n            }\n        });\n        return {\n            'existing_question_id': this.$el.data('id'),\n            'sequence': this.sequence,\n            'question': $form.find('.o_wslides_quiz_question input[type=text]').val(),\n            'slide_id': this.slideId,\n            'answer_ids': answers\n        };\n    },\n\n});\n\nexport default QuestionFormWidget;\n", "/** @odoo-module **/\n\nimport { CourseTagAddDialog } from \"@website_slides/js/public/components/course_tag_add_dialog/course_tag_add_dialog\";\nimport publicWidget from '@web/legacy/js/public/public_widget';\n\npublicWidget.registry.websiteSlidesTag = publicWidget.Widget.extend({\n    selector: '.o_wslides_js_channel_tag_add',\n    events: {\n        'click': '_onAddTagClick',\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onAddTagClick: function (ev) {\n        ev.preventDefault();\n        const channelTagIds = ev.currentTarget.dataset.channelTagIds;\n        this.call(\"dialog\", \"add\", CourseTagAddDialog, {\n            channelId: parseInt(ev.currentTarget.dataset.channelId, 10),\n            tagIds: channelTagIds ? JSON.parse(channelTagIds) : [],\n        });\n    },\n});\n\nexport default {\n    websiteSlidesTag: publicWidget.registry.websiteSlidesTag,\n};\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { SlideUnsubscribeDialog } from \"./public/components/slide_unsubscribe_dialog/slide_unsubscribe_dialog\";\n\npublicWidget.registry.websiteSlidesUnsubscribe = publicWidget.Widget.extend({\n    selector: '.o_wslides_js_channel_unsubscribe',\n    events: {\n        'click': '_onUnsubscribeClick',\n    },\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _openDialog: function ($element) {\n        var data = $element.data();\n        this.call(\"dialog\", \"add\", SlideUnsubscribeDialog, data);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onUnsubscribeClick: function (ev) {\n        ev.preventDefault();\n        this._openDialog($(ev.currentTarget));\n    },\n});\n\nexport default {\n    websiteSlidesUnsubscribe: publicWidget.registry.websiteSlidesUnsubscribe\n};\n", "import RatingPopupComposer from \"@portal_rating/js/portal_rating_composer\";\n\nRatingPopupComposer.include({\n    _update_options: function (data) {\n        this._super(...arguments);\n        this.options.force_submit_url =\n            data.force_submit_url ||\n            (this.options.default_message_id && \"/slides/mail/update_comment\");\n    },\n});\n", "/** @odoo-module **/\n\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\n\nexport class CategoryAddDialog extends ConfirmationDialog {\n    static template = \"website_slides.CategoryAddDialog\";\n    static props = {\n        ...ConfirmationDialog.props,\n        channelId: String,\n    };\n\n    setup() {\n        super.setup();\n        this.inputRef = useAutofocus();\n        this.csrf_token = odoo.csrf_token;\n        this.lastInputValue;\n    }\n\n    _confirm() {\n        this.execButton(() => {\n            if (this.inputRef.el.value === this.lastInputValue) {\n                return;\n            }\n            this.lastInputValue = this.inputRef.el.value;\n            return this.props.confirm({ formEl: this.modalRef.el.querySelector(\"form\") });\n        });\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport class CourseTagAddDialog extends Component {\n    static components = { Dialog, DropdownItem, SelectMenu };\n    static props = {\n        channelId: { type: Number, optional: true },\n        defaultTag: { type: String, optional: true },\n        tagIds: Array,\n        close: Function,\n    };\n    static template = \"website_slides.CourseTagAddDialog\";\n\n    async setup() {\n        super.setup();\n        this.choices = useState({\n            tagIds: [],\n            tagGroupIds: [],\n            tagId: null,\n            tagGroupId: null,\n        });\n        this.state = useState({\n            showTagGroup: false,\n            canCreateTagGroup: false,\n            canCreateTag: false,\n            alertMsg: \"\",\n        });\n        this.validation = useState({\n            tagIsValid: undefined,\n            tagGroupIsValid: undefined,\n        });\n        const [tags, groups] = await Promise.all([\n            this._fetchChoices(\"tag\", [\n                [\"id\", \"not in\", this.props.tagIds],\n                [\"color\", \"!=\", 0],\n            ]),\n            this._fetchChoices(\"tag/group\"),\n        ]);\n        this.choices.tagIds = tags.choices;\n        this.state.canCreateTag = tags.can_create;\n        this.choices.tagGroupIds = groups.choices;\n        this.state.canCreateTagGroup = groups.can_create;\n\n        if (this.props.defaultTag) {\n            // Note: when a default tag is passed to the props we want the tag SelectMenu to behave\n            // like a 'readonly' selectMenu dropdown (can see the options but cannot change the selection)\n            this.createChoice(this.props.defaultTag);\n            this.state.canCreateTag = false;\n        }\n    }\n\n    get displayTagValue() {\n        return this.choices.tagId\n            ? this.choices.tagIds.find((t) => t.value === this.choices.tagId).label\n            : _t(\"Select or create a tag\");\n    }\n\n    get displayTagGroupValue() {\n        return this.choices.tagGroupId\n            ? this.choices.tagGroupIds.find((t) => t.value === this.choices.tagGroupId).label\n            : _t(\"Select or create a tag group\");\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    onClickFormSubmit() {\n        this.state.alertMsg = \"\";\n        if (!this._formValidate()) {\n            return;\n        }\n        const values = this._getSelectMenuValues();\n        if (this.props.defaultTag && !this.channelId) {\n            this._createNewTag(values);\n        } else {\n            this._addTagToChannel(values);\n        }\n    }\n\n    /**\n     * Create a new choice for a given select menu (type) and select it.\n     * Also display tag group select\n     * @param {String} label\n     * @param {String} type\n     */\n    createChoice(label, type = \"tag\") {\n        const tempId = uniqueId(\"temp\");\n        this.choices[`${type}Ids`].push({ value: tempId, label: label });\n        this.choices[`${type}Id`] = tempId;\n        this.state.showTagGroup = true;\n    }\n\n    /**\n     * Set the tagId value and displays the tagGroup Select Menu when appropriate\n     * @param {*} value\n     */\n    onTagSelect(value) {\n        if (!this.props.defaultTag) {\n            this.choices.tagId = value;\n            this.state.showTagGroup = this._toCreate(value) ? true : false;\n        }\n    }\n\n    /**\n     * Set the tagGroupId\n     * @param {*} value\n     */\n    onTagGroupSelect(value) {\n        this.choices.tagGroupId = value;\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Object} values\n     */\n    async _addTagToChannel(values) {\n        const data = await rpc(\"/slides/channel/tag/add\", {\n            channel_id: this.props.channelId,\n            ...values,\n        });\n\n        if (data.error) {\n            this.state.alertMsg = data.error;\n        } else {\n            window.location.reload();\n        }\n    }\n\n    /**\n     * @private\n     * @param {Object} values\n     */\n    async _createNewTag(values) {\n        const data = await rpc(\"/slide_channel_tag/add\", values);\n\n        if (data.error) {\n            this.state.alertMsg = data.error;\n        } else {\n            this.props.close();\n        }\n    }\n\n    /**\n     * @private\n     * @returns Boolean\n     */\n    _formValidate() {\n        for (const key in this.validation) {\n            this.validation[key] = undefined;\n        }\n        if (!this.choices.tagId) {\n            this.validation.tagIsValid = false;\n            return false;\n        }\n        this.validation.tagIsValid = true;\n        if (this.state.showTagGroup) {\n            if (!this.choices.tagGroupId) {\n                this.validation.tagGroupIsValid = false;\n                return false;\n            }\n            this.validation.tagGroupIsValid = true;\n        }\n        return true;\n    }\n\n    /**\n     * @private\n     * @param {String} type\n     * @param {Array} domain\n     * @param {Array} fields\n     * @returns {Object} result\n     */\n    async _fetchChoices(type, domain = [], fields = [\"name\"]) {\n        const { read_results, can_create } = await rpc(`/slides/channel/${type}/search_read`, {\n            fields,\n            domain,\n        });\n\n        const choices = read_results.map((choice) => {\n            return { value: choice.id, label: choice.name };\n        });\n        return { choices, can_create };\n    }\n\n    /**\n     * Get value for tagId and [when appropriate] tagGroupId to send to server\n     * @private\n     */\n    _getSelectMenuValues() {\n        const tag = this.choices.tagIds.find((c) => c.value === this.choices.tagId);\n        if (!tag) {\n            return {};\n        }\n        if (!this._toCreate(tag.value)) {\n            // existing tag\n            return { tag_id: [tag.value] };\n        }\n        const group = this.choices.tagGroupIds.find((c) => c.value === this.choices.tagGroupId);\n        if (!group) {\n            return {};\n        }\n        return {\n            tag_id: [0, { name: tag.label }],\n            group_id: this._toCreate(group.value) ? [0, { name: group.label }] : [group.value],\n        };\n    }\n\n    /**\n     * @private\n     * @param {*} value\n     * @returns Boolean\n     */\n    _toCreate(value) {\n        return typeof value === \"string\" && value.startsWith(\"temp\");\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Component, onMounted, useState } from \"@odoo/owl\";\nimport { SlideXPProgressBar } from \"@website_slides/js/public/components/slide_quiz_finish_dialog/slide_xp_progress_bar\";\n\nexport class SlideQuizFinishDialog extends Component {\n    static components = { Dialog, SlideXPProgressBar };\n    static props = {\n        close: Function,\n        hasNext: Boolean,\n        onClickNext: Function,\n        quiz: Object,\n        userId: Number,\n    };\n    static template = \"website_slides.SlideQuizFinishDialog\";\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            animateKarmaGain: false,\n            fadeRankMotivational: false,\n            hideDismissBtns: true,\n            showRankMotivational: false,\n        });\n        this.title = this.props.quiz.rankProgress.level_up ? _t(\"Level up!\") : _t(\"Amazing!\");\n        onMounted(() => this.animateText());\n    }\n\n    //--------------------------------\n    // Handler\n    //--------------------------------\n\n    onClickNext() {\n        this.props.onClickNext();\n        this.props.close();\n    }\n\n    //--------------------------------\n    // Business methods\n    //--------------------------------\n\n    /**\n     * Handles the animation of the different text such as the karma gain\n     * and the motivational message when the user levels up.\n     * @public\n     */\n    animateText() {\n        browser.setTimeout(() => {\n            this.state.animateKarmaGain = true;\n            this.state.hideDismissBtns = false;\n        }, 800);\n\n        if (this.props.quiz.rankProgress.level_up) {\n            browser.setTimeout(() => {\n                this.state.fadeRankMotivational = true;\n                browser.setTimeout(() => {\n                    this.state.showRankMotivational = true;\n                }, 800);\n            }, 800);\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { Component, onMounted, useState } from \"@odoo/owl\";\n\nexport class SlideXPProgressBar extends Component {\n    static props = {\n        previousRank: Object,\n        newRank: Object,\n        levelUp: Boolean,\n    };\n    static template = \"website_slides.SlideXPProgressBar\";\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            hideRankBounds: true,\n            rankLowerBound: this.props.previousRank.lower_bound,\n            rankProgressPercentage: this.props.previousRank.progress,\n            userKarma: this.props.previousRank.karma,\n            rankUpperBound: this.props.previousRank.upper_bound,\n        });\n        onMounted(() => {\n            this.animateProgressBar();\n        });\n    }\n\n    //--------------------------------\n    // Business methods\n    //--------------------------------\n\n    /**\n     * Handles the animation of the karma gain in the following steps:\n     * 1. Animate the tooltip text to increment smoothly from the old\n     *    karma value to the new karma value.\n     * 2a. The user doesn't level up\n     *    I.   When the user doesn't level up the progress bar simply goes\n     *         from the old karma value to the new karma value.\n     * 2b. The user levels up\n     *    I.   The first step makes the progress bar go from the old karma\n     *         value to 100%.\n     *    II.  The second step makes the progress bar go from 100% to 0%.\n     *    III. The third and final step makes the progress bar go from 0%\n     *         to the new karma value. It also changes the lower and upper\n     *         bound to match the new rank.\n     * @public\n     */\n    animateProgressBar() {\n        // tooltip: karma incrementation\n        const duration = this.props.levelUp ? 1700 : 800;\n        const startTime = Date.now();\n\n        const animateKarma = () => {\n            const progress = (Date.now() - startTime) / duration;\n            if (progress >= 1) {\n                this.state.userKarma = this.props.newRank.karma;\n            } else {\n                this.state.userKarma = Math.ceil(\n                    this.props.previousRank.karma +\n                        (this.props.newRank.karma - this.props.previousRank.karma) * progress\n                );\n                browser.requestAnimationFrame(animateKarma);\n            }\n        };\n\n        // progress bar and tooltip animations\n        this.state.hideRankBounds = false;\n        browser.requestAnimationFrame(animateKarma);\n        this.state.rankProgressPercentage = this.props.newRank.progress;\n\n        if (this.props.levelUp) {\n            browser.setTimeout(() => {\n                this.state.rankLowerBound = this.props.newRank.lower_bound;\n                this.state.rankUpperBound = this.props.newRank.upper_bound;\n            }, 800);\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { session } from \"@web/session\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { Component, useRef, useState } from \"@odoo/owl\";\n\nexport class EmailSharingInput extends Component {\n    static template = \"website_slides.EmailSharingInput\";\n    static props = {\n        id: { type: Number },\n        isChannel: { type: Boolean, optional: true },\n        isFullscreen: { type: Boolean, optional: true },\n        category: { type: String, optional: true },\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.input = useRef(\"input\");\n        this.isWebsiteUser = session.is_website_user;\n        this.state = useState({\n            isDone: false,\n            isInvalid: false,\n        });\n    }\n\n    onKeyPress(event) {\n        if (event.key === \"Enter\") {\n            event.preventDefault();\n            this.onShareByEmailClick();\n        }\n    }\n\n    async onShareByEmailClick() {\n        const emails = this.input.el.value;\n        if (emails) {\n            const type = this.props.isChannel ? \"channel\" : \"slide\";\n            const done = await rpc(`/slides/${type}/send_share_email`, {\n                emails: emails,\n                fullscreen: this.props.isFullscreen,\n                [`${type}_id`]: this.props.id,\n            });\n            this.state.isDone = done;\n            if (done) {\n                return;\n            }\n        }\n        this.setInvalid();\n    }\n\n    setInvalid() {\n        this.state.isInvalid = true;\n        this.notification.add(_t(\"Please enter valid email(s)\"), { type: \"danger\" });\n        this.input.el.focus();\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { CopyButton } from \"@web/core/copy_button/copy_button\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { EmailSharingInput } from \"./email_sharing_input\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nexport class SlideShareDialog extends Component {\n    static template = \"website_slides.SlideShareDialog\";\n    static components = { Dialog, CopyButton, EmailSharingInput };\n    static props = {\n        category: { type: String, optional: true },\n        close: { type: Function },\n        documentMaxPage: { type: Number, optional: true },\n        emailSharing: { type: Boolean, optional: true },\n        embedCode: { type: String, optional: true },\n        id: { type: Number },\n        isChannel: { type: Boolean, optional: true },\n        isFullscreen: { type: Boolean, optional: true },\n        name: { type: String },\n        url: { type: String },\n    };\n\n    setup() {\n        this.codeInput = useRef(\"codeInput\");\n        this.copyUrlText = _t(\"Copy Link\");\n        this.copyEmbedCodeText = _t(\"Copy Embed Code\");\n        this.successText = _t(\"Copied\");\n    }\n\n    onSocialShareClick(url) {\n        browser.open(url, \"Share Dialog\", \"width=626,height=436\");\n    }\n\n    onPageChange(event) {\n        const page = event.currentTarget.value;\n        const newEmbedCodeValue = this.codeInput.el.value.replace(/(page=).*?([^\\d]+)/, \"$1\" + page + \"$2\");\n        this.codeInput.el.value = newEmbedCodeValue;\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport class SlideUnsubscribeDialog extends Component {\n    static template = \"website_slides.SlideUnsubscribeDialog\";\n    static components = { CheckBox, Dialog };\n    static props = {\n        channelId: Number,\n        isFollower: { type: String, optional: true },\n        visibility: String,\n        enroll: { type: String, optional: true },\n        close: Function,\n    };\n\n    setup() {\n        this.state = useState({\n            buttonDisabled: false,\n        });\n        this.channelID = parseInt(this.props.channelId, 10);\n        this.isFollower = this.props.isFollower === \"True\";\n        this.updateState(\"subscription\");\n        this.isChecked = this.isFollower;\n    }\n\n    updateState(mode) {\n        if (mode === \"subscription\") {\n            this.state.title = this.isFollower ? _t(\"Subscribe\") : _t(\"Notifications\");\n            this.state.mode = \"subscription\";\n        } else if (mode === \"leave\") {\n            this.state.title = _t(\"Leave the course\");\n            this.state.mode = \"leave\";\n        }\n    }\n\n    onChangeCheckbox(isChecked) {\n        this.isChecked = isChecked;\n    }\n\n    onClickLeaveCourse() {\n        this.updateState(\"leave\");\n    }\n\n    onClickLeaveCourseCancel() {\n        this.updateState(\"subscription\");\n    }\n\n    async onClickLeaveCourseSubmit() {\n        if (this.state.buttonDisabled) {\n            return;\n        }\n        this.state.buttonDisabled = true;\n\n        await rpc(\"/slides/channel/leave\", { channel_id: this.channelID });\n        if (this.props.visibility === \"public\" || this.props.visibility === \"connected\") {\n            window.location.reload();\n        } else {\n            window.location.href = \"/slides\";\n        }\n    }\n\n    async onClickSubscriptionSubmit() {\n        if (this.state.buttonDisabled) {\n            return;\n        }\n        this.state.buttonDisabled = true;\n\n        if (this.isFollower === this.isChecked) {\n            this.props.close();\n        } else {\n            await rpc(`/slides/channel/${this.isChecked ? \"subscribe\" : \"unsubscribe\"}`, {\n                channel_id: this.channelID,\n            });\n            window.location.reload();\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { redirect } from \"@web/core/utils/urls\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class SlideInstallModule extends Component {\n    static components = {};\n    static props = {\n        moduleData: {\n            name: String,\n            id: Number,\n            default_slide_category: { type: String, optional: true },\n        },\n    };\n    static template = \"website_slides.SlideInstallModule\";\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            status: \"start\", // \"failure\", \"installing\"\n            message: _t('Do you want to install \"%s\"?', this.props.moduleData.name),\n        });\n    }\n\n    async installModule() {\n        if (this.state.status === \"installing\") {\n            return;\n        }\n        this.state.status = \"installing\";\n        this.state.message = _t('Installing \"%s\"...', this.props.moduleData.name);\n        try {\n            await this.orm.call(\"ir.module.module\", \"button_immediate_install\", [\n                [this.props.moduleData.id],\n            ]);\n        } catch {\n            this.state.hasFailed = \"failure\";\n            this.state.message = _t('Failed to install \"%s\"', this.props.moduleData.name);\n            return;\n        }\n        let redirectUrl = window.location.origin + window.location.pathname;\n        if (this.props.moduleData.default_slide_category) {\n            redirectUrl += \"?enable_slide_upload=\" + this.props.moduleData.default_slide_category;\n        }\n        redirect(redirectUrl);\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, onMounted, onWillStart, useState } from \"@odoo/owl\";\nimport { getDataURLFromFile } from \"@web/core/utils/urls\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { SlideUploadSourceTypes } from \"./slide_upload_source_types\";\nimport { SlideUploadSelectTags } from \"./slide_upload_select_tags\";\n\nexport class SlideUploadCategory extends Component {\n    static components = { DropdownItem, SelectMenu, SlideUploadSelectTags, SlideUploadSourceTypes };\n    static props = {\n        alertMsg: { type: String, optional: true },\n        channelId: Number,\n        categoryId: { type: String, optional: true },\n        slideCategory: String,\n        canPublish: Boolean,\n        canUpload: Boolean,\n        upload: Function,\n        slots: Object,\n    };\n    static sourceSettings = {\n        document: {\n            sourceTypeLabel: _t(\"Document Source\"),\n            selectFileLabel: _t(\"Choose a PDF\"),\n            acceptedFiles: \"application/pdf\",\n            urlInputLabel: _t(\"Document Link\"),\n            urlInputName: \"document_google_url\",\n        },\n        infographic: {\n            sourceTypeLabel: _t(\"Image Source\"),\n            selectFileLabel: _t(\"Choose an Image\"),\n            acceptedFiles: \"image/*\",\n            urlInputLabel: _t(\"Image Link\"),\n            urlInputName: \"image_google_url\",\n        },\n        video: {\n            urlInputLabel: _t(\"Video Link\"),\n            urlInputName: \"video_url\",\n        },\n    };\n    static template = \"website_slides.SlideUploadCategory\";\n\n    setup() {\n        this.sourceSettings = SlideUploadCategory.sourceSettings;\n        this.state = useState({\n            alert: {\n                class: \"\",\n                message: \"\",\n                show: false,\n            },\n            form: {\n                duration: null,\n                isLoading: false,\n                isLocalSource: true,\n                slideImage: \"/website_slides/static/src/img/document.png\",\n                slideName: \"\",\n                wasValidated: false,\n                url: \"\",\n            },\n            preview: {\n                show: false,\n                hideSlideVideoTitle: true,\n                videoTitle: \"\",\n            },\n            choices: {\n                categories: [],\n                categoryId: \"\",\n                tags: [],\n                tagIds: [],\n            },\n        });\n        this.canSubmitForm = false;\n        this.defaultCategoryId = parseInt(this.props.categoryId, 10);\n        this.file = {};\n        this.isValidUrl = true;\n\n        onWillStart(async () => {\n            const categories = await this._fetch_choices(\"category\", [\n                [\"channel_id\", \"=\", this.props.channelId],\n            ]);\n            this.state.choices.categories = categories;\n            this.state.choices.categoryId = this._getDefaultCategoryId();\n            const tags = await this._fetch_choices(\"tag\");\n            this.state.choices.tags = tags;\n        });\n\n        onMounted(() => {\n            if (this.props.alertMsg) {\n                this._alertDisplay(this.props.alertMsg);\n            }\n        });\n    }\n\n    /**\n     * To figure when to propose users to create a new category or tag\n     */\n    choiceExists(input, choices) {\n        return choices.some((choice) => input.toLowerCase() === choice.label.toLowerCase());\n    }\n\n    get displayCategoryValue() {\n        return this.state.choices.categoryId\n            ? this.state.choices.categories.find((c) => c.value === this.state.choices.categoryId)\n                  .label\n            : _t(\"Select or create a category\");\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    // Category and tag SelectMenus\n\n    onCategorySelect(value) {\n        this.state.choices.categoryId = value;\n    }\n\n    onClickCreateCategoryBtn(categoryName) {\n        const tempId = uniqueId(\"temp\");\n        this.state.choices.categories.push({ value: tempId, label: categoryName });\n        this.state.choices.categoryId = tempId;\n    }\n\n    onTagsSelect(values) {\n        this.state.choices.tagIds = values;\n    }\n\n    onClickCreateTagBtn(tagName) {\n        const tempId = uniqueId(\"temp\");\n        this.state.choices.tags.push({ value: tempId, label: tagName });\n        this.state.choices.tagIds.push(tempId);\n    }\n\n    // Form\n\n    async onChangeFileInput(ev) {\n        this._alertRemove();\n\n        const preventOnchange = ev.currentTarget.dataset.preventOnchange;\n\n        const file = ev.target.files[0];\n        if (!file) {\n            this.state.form.slideImage = \"/website_slides/static/src/img/document.png\";\n            this.state.preview.show = false;\n            return;\n        }\n        const isImage = /^image\\/.*/.test(file.type);\n        let loaded = false;\n        this.file.name = file.name;\n        this.file.type = file.type;\n        if (!isImage && this.file.type !== \"application/pdf\") {\n            this._alertDisplay(_t(\"Invalid file type. Please select pdf or image file\"));\n            this._fileReset();\n            this.state.preview.show = false;\n            return;\n        }\n        if (file.size > 25 * 1024 * 1024) {\n            this._alertDisplay(_t(\"File is too big. File size cannot exceed 25MB\"));\n            this._fileReset();\n            this.state.preview.show = false;\n            return;\n        }\n\n        if (file.type !== \"application/pdf\") {\n            const dataURL = await getDataURLFromFile(file);\n            if (isImage) {\n                this.state.form.slideImage = dataURL;\n            }\n            this.file.data = dataURL.split(\",\", 2)[1];\n            this.state.preview.show = true;\n        } else {\n            this.canSubmitForm = false;\n            const dataURL = await getDataURLFromFile(file);\n            this.file.data = dataURL.split(\",\", 2)[1];\n            /**\n             * The following line fixes pdfjsLib 'Util' global variable.\n             * This is (most likely) related to #32181 which lazy loads most assets.\n             * See commit 3716a9b\n             */\n            window.Util = window.pdfjsLib.Util;\n            // pdf is stored in file.data in base64 and converted in binary (atob) to generate the preview\n            const pdfTask = window.pdfjsLib.getDocument({ data: atob(this.file.data) });\n            pdfTask.onPassword = () => {\n                this._alertDisplay(_t(\"You can not upload password protected file.\"));\n                this._fileReset();\n                this.canSubmitForm = true;\n            };\n            const pdf = await pdfTask.promise;\n\n            this.state.form.duration = (pdf.numPages || 0) * 5;\n            const page = await pdf.getPage(1);\n            const viewport = page.getViewport({ scale: 1 });\n            const canvas = document.getElementById(\"data_canvas\");\n            const context = canvas.getContext(\"2d\");\n            canvas.height = viewport.height;\n            canvas.width = viewport.width;\n            // Render PDF page into canvas context\n            await page.render({\n                canvasContext: context,\n                viewport: viewport,\n            }).promise;\n            this.state.form.slideImage = canvas.toDataURL();\n            if (loaded) {\n                this.canSubmitForm = true;\n            }\n            loaded = true;\n            this.state.preview.show = true;\n        }\n\n        if (!preventOnchange) {\n            const input = file.name;\n            const inputVal = input.substr(0, input.lastIndexOf(\".\")) || input;\n            if (this.state.form.slideName === \"\") {\n                this.state.form.slideName = inputVal;\n            }\n        }\n    }\n\n    /**\n     * When the URL changes for slides of categories infographic, document and video, we attempt to fetch\n     * some metadata on YouTube / Google Drive (such as a name, a title, a duration, ...).\n     */\n    async onChangeUrl(url) {\n        this._alertRemove();\n        this.isValidUrl = false;\n        this.canSubmitForm = false;\n        this.state.form.isLoading = true;\n        this.state.form.url = url;\n        const data = await rpc(\"/slides/prepare_preview/\", {\n            url: this.state.form.url,\n            slide_category: this.props.slideCategory,\n            channel_id: this.props.channelId,\n        });\n        this.canSubmitForm = true;\n        if (data.error) {\n            this._alertDisplay(data.error);\n            this.state.preview.show = false;\n        } else {\n            if (data.info) {\n                this._alertDisplay(data.info, \"alert-info\");\n            } else {\n                this._alertRemove();\n            }\n\n            this.isValidUrl = true;\n\n            if (data.name) {\n                this.state.form.slideName = data.name;\n                this.state.preview.videoTitle = data.name;\n                this.state.preview.hideSlideVideoTitle = false;\n            } else {\n                this.state.preview.hideSlideVideoTitle = true;\n            }\n\n            if (data.completion_time) {\n                // hours to minutes conversion\n                this.state.form.duration = Math.round(data.completion_time * 60);\n            }\n            if (data.image_url) {\n                this.state.form.slideImage = data.image_url;\n            }\n\n            if (!data.name && !data.image_url) {\n                this.state.preview.show = false;\n            } else {\n                this.state.preview.show = true;\n            }\n        }\n\n        this.state.form.isLoading = false;\n    }\n\n    async onClickFormSubmit(forcePublished) {\n        if (!this._formValidate()) {\n            return;\n        }\n        const values = await this._formValidateGetValues(forcePublished);\n        this.props.upload(values, this.props.slideCategory);\n    }\n\n    /**\n     * When the user selects 'local_file' or 'external' as source type, we display the 'upload'\n     * field or the 'document_google_url' / 'image_google_url' fields respectively.\n     */\n    onClickSourceType(isLocalSource) {\n        this.state.form.isLocalSource = isLocalSource;\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    // Alert messages\n\n    /**\n     * @param {string} message\n     */\n    _alertDisplay(message, alertClass = \"alert-warning\") {\n        this.state.alert.message = message;\n        this.state.alert.class = alertClass;\n        this.state.alert.show = true;\n    }\n\n    _alertRemove() {\n        this.state.alert.show = false;\n        this.state.alert.message = \"\";\n        this.state.alert.class = \"\";\n    }\n\n    // Category and tag SelectMenus\n\n    /**\n     * Get value for category_id and tag_ids (ORM cmd) to send to server\n     */\n    _getSelectMenuValues() {\n        const result = {};\n        // tags\n        if (this.state.choices.tagIds.length > 0) {\n            const tags = Object.fromEntries(\n                this.state.choices.tags.map((tag) => [tag.value, tag.label])\n            );\n            result.tag_ids = this.state.choices.tagIds.map((tagId) =>\n                this._toCreate(tagId) ? [0, 0, { name: tags[tagId] }] : [4, tagId]\n            );\n        }\n        // category\n        if (!this.defaultCategoryId) {\n            if (this._toCreate(this.state.choices.categoryId)) {\n                const category = this.state.choices.categories.find(\n                    (cat) => cat.value === this.state.choices.categoryId\n                );\n                result.category_id = [0, { name: category.label }];\n            } else {\n                const categoryId = this.state.choices.categoryId || this._getDefaultCategoryId();\n                result.category_id = [categoryId];\n            }\n        } else {\n            result.category_id = [this.defaultCategoryId];\n        }\n        return result;\n    }\n\n    /**\n     * Returns the id of the last section of the channel or null (no sections)\n     * @returns {Number|Null}\n     */\n    _getDefaultCategoryId() {\n        return this.state.choices.categories.length > 0\n            ? this.state.choices.categories[this.state.choices.categories.length - 1].value\n            : null;\n    }\n\n    /**\n     * Fetch available course categories and tags\n     */\n    async _fetch_choices(type, domain = [], fields = [\"name\"]) {\n        const results = await rpc(`/slides/${type}/search_read`, { fields, domain });\n\n        return results.read_results.map((choice) => {\n            return { value: choice.id, label: choice.name };\n        });\n    }\n\n    /**\n     * Check whether it is a new category/tag or not\n     */\n    _toCreate(value) {\n        return typeof value === \"string\" && value.startsWith(\"temp\");\n    }\n\n    // Form\n\n    _fileReset() {\n        document.getElementById(\"upload\").value = \"\";\n        this.file.name = false;\n    }\n\n    _formValidate() {\n        this.state.form.wasValidated = true;\n        return (\n            document.querySelector(\"#o_w_slide_upload_category_form\").checkValidity() &&\n            this.isValidUrl\n        );\n    }\n\n    /**\n     * Extract values to submit from form, force the slide_category according to\n     * filled values.\n     * @param {boolean} forcePublished\n     */\n    async _formValidateGetValues(forcePublished) {\n        let sourceType = \"local_file\";\n        if (this.props.slideCategory === \"video\") {\n            sourceType = \"external\"; // force external for videos\n        } else {\n            sourceType = this.state.form.isLocalSource ? \"local_file\" : \"external\";\n        }\n        const values = Object.assign(\n            {\n                channel_id: this.props.channelId,\n                document_google_url: this.state.form.url,\n                duration: this.state.form.duration,\n                image_google_url: this.state.form.url,\n                is_published: forcePublished,\n                name: this.state.form.slideName,\n                slide_category: this.props.slideCategory,\n                source_type: sourceType,\n                video_url: this.state.form.url,\n            },\n            this._getSelectMenuValues()\n        ); // add tags and category\n\n        if (this.file.type === \"application/pdf\") {\n            Object.assign(values, {\n                image_1920: document.getElementById(\"data_canvas\").toDataURL().split(\",\")[1],\n                slide_category: \"document\",\n                binary_content: this.file.data,\n            });\n        } else if (/^image\\/.*/.test(this.file.type)) {\n            Object.assign(values, {\n                slide_category: \"infographic\",\n                binary_content: this.file.data,\n            });\n        }\n        return values;\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, onMounted, useState } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { redirect } from \"@web/core/utils/urls\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { ModuleToInstallIcon, SlideCategoryIcon } from \"./slide_upload_dialog_select\";\nimport { SlideInstallModule } from \"./slide_install_module\";\nimport { SlideUploadCategory } from \"./slide_upload_category\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport class SlideUploadDialog extends Component {\n    static baseSettings = {\n        modulesToInstallMsg: \"\",\n        page: \"select\",\n        size: \"md\",\n        alertMsg: \"\",\n        title: _t(\"Add Content\"),\n        installModuleData: null,\n    };\n    static categoryData = {\n        document: { icon: \"fa-file-pdf-o\", label: _t(\"Document\") },\n        infographic: { icon: \"fa-file-image-o\", label: _t(\"Image\") },\n        article: { icon: \"fa-file-text\", label: _t(\"Article\") },\n        video: { icon: \"fa-file-video-o\", label: _t(\"Video\") },\n        quiz: { icon: \"fa-question-circle\", label: _t(\"Quiz\") },\n    };\n    static components = {\n        Dialog,\n        DropdownItem,\n        ModuleToInstallIcon,\n        SelectMenu,\n        SlideCategoryIcon,\n        SlideUploadCategory,\n        SlideInstallModule,\n    };\n    static pagesTemplates = {\n        article: \"website_slides.SlideCategoryTutorial.Article\",\n        document: \"website_slides.SlideCategoryTutorial.Document\",\n        infographic: \"website_slides.SlideCategoryTutorial.Infographic\",\n        select: \"website_slides.SlideUploadDialogSelect\",\n        install_module: \"website_slides.UploadDialogInstallModule\",\n        upload: \"website_slides.UploadInProgressDialog\",\n        video: \"website_slides.SlideCategoryTutorial.Video\",\n        quiz: \"website_slides.SlideCategoryTutorial.Quiz\",\n    };\n    static props = {\n        canPublish: Boolean,\n        canUpload: Boolean,\n        categoryId: { type: String, optional: true },\n        channelId: Number,\n        close: Function,\n        modulesToInstall: { type: Array, optional: true },\n        openModal: { type: String, optional: true },\n    };\n    static template = \"website_slides.SlideUploadDialog\";\n\n    setup() {\n        this.defaultCategoryID = parseInt(this.props.categoryId, 10);\n        this.modulesToInstallStatus = null;\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.pagesTemplates = this.constructor.pagesTemplates;\n        this.slideCategoryData = this.constructor.categoryData;\n        this.state = useState({ ...this.constructor.baseSettings });\n        onMounted(() => {\n            if (this.props.openModal && this.props.openModal in this.slideCategoryData) {\n                // Sets the appropriate category's upload template if one has to be opened on load.\n                this.onClickSlideCategoryIcon(this.props.openModal);\n            }\n        });\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    onClickSlideCategoryIcon(slideCategory) {\n        this.state.page = slideCategory;\n        this.state.size = \"lg\";\n    }\n\n    onClickInstallModuleIcon(moduleId) {\n        this.state.page = \"install_module\";\n        this.state.installModuleData = this.props.modulesToInstall.find((m) => m.id === moduleId);\n        this.state.size = \"md\";\n    }\n\n    onClickGoBack() {\n        Object.assign(this.state, SlideUploadDialog.baseSettings);\n    }\n\n    /**\n     * Show the upload page while processing new slide submission\n     */\n    async uploadSlide(formValues, previousPage) {\n        this.state.page = \"upload\";\n        this.state.size = \"md\";\n        const data = await rpc(\"/slides/add_slide\", formValues);\n        if (data.error) {\n            this.state.page = previousPage;\n            this.state.size = \"lg\";\n            this.state.alertMsg = data.error;\n            return;\n        }\n        if (data.url.includes(\"enable_editor\")) {\n            // If we need to enter edit mode, it should be done to the top\n            // window so that we end up refreshing the backend client action\n            // in edit mode.\n            const { origin, pathname } = window.top.location;\n            const url = new URL(data.url, `${origin}${pathname}`);\n            if (url.origin === origin) {\n                window.top.location = url.href;\n            }\n        } else {\n            redirect(data.url);\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\n\nexport class ModuleToInstallIcon extends Component {\n    static template = \"website_slides.ModuleToInstallIcon\";\n    static props = {\n        title: String,\n        moduleId: Number,\n        motivational: String,\n        onClickInstallModuleIcon: Function,\n    };\n}\n\nexport class SlideCategoryIcon extends Component {\n    static template = \"website_slides.SlideCategoryIcon\";\n    static props = {\n        slideCategory: String,\n        categoryData: {\n            type: Object,\n            shape: {\n                icon: String,\n                label: String,\n            },\n        },\n        onClickSlideCategoryIcon: Function,\n    };\n}\n", "/** @odoo-module **/\n\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\n\nexport class SlideUploadSelectTags extends SelectMenu {}\nSlideUploadSelectTags.template = \"website_slides.SlideUploadSelectTags\";\n", "/** @odoo-module **/\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class SlideUploadSourceTypes extends Component {\n    static props = {\n        attributes: {\n            type: Object,\n            shape: {\n                sourceTypeLabel: { type: String, optional: true },\n                selectFileLabel: { type: String, optional: true },\n                acceptedFiles: { type: String, optional: true },\n                urlInputLabel: String,\n                urlInputName: String,\n            },\n        },\n        isLocalSource: Boolean,\n        onClickSourceType: Function,\n        onChangeFileInput: Function,\n        onChangeUrl: Function,\n    };\n    static template = \"website_slides.SlideUploadSourceTypes\";\n\n    setup() {\n        this.state = useState({ url: \"\" });\n    }\n}\n", "/** @odoo-module **/\n/* eslint-disable no-undef */\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { loadBundle } from \"@web/core/assets\";\nconst { DateTime } = luxon;\n\npublicWidget.registry.PlanningView = publicWidget.Widget.extend({\n    selector: '#calendar_employee',\n\n    init: function (parent, options) {\n        this._super.apply(this, arguments);\n    },\n    start: function () {\n        if ($('.message_slug').attr('value')) {\n            $(\"#PlanningToast\").toast('show');\n        }\n        this._super.apply(this, arguments);\n        // The calendar is displayed if there are slots (open or not)\n        if ($('.no_data').attr('value')) {\n            return;\n        }\n        this.calendarElement = this.$(\".o_calendar_widget\")[0];\n        const employeeSlotsFcData = JSON.parse($('.employee_slots_fullcalendar_data').attr('value'));\n        const locale = $('.locale').attr('value');\n        // initialise popovers and add the event listeners\n        $('[data-bs-toggle=\"popover\"]').popover();\n        // code used to dismiss popover when clicking outside of it\n        $('body').on('click', function (e) {\n            var parentElsClassList = $(e.target).parents().map(function() {\n                return [...this.classList];\n            })\n            if (!['assignee-cell', 'contact-assignee-popover'].some(el => [...parentElsClassList].includes(el))) {\n                $('[data-bs-toggle=\"popover\"]').popover('hide');\n            }\n        });\n        // code used to dismiss popover when opening another popover\n        $('[data-bs-toggle=\"popover\"]').on('click', function (e) {\n            $('[data-bs-toggle=\"popover\"]').not(this).popover('hide');\n        });\n        // default date: first event of either assigned slots or open shifts\n        const defaultStartValue = $('.default_start').attr('value'); //yyyy-MM-dd\n        const defaultStart = DateTime.fromFormat(defaultStartValue, \"yyyy-MM-dd\").toJSDate();\n        const defaultView = $('.default_view').attr('value');\n        const minTime = $('.mintime').attr('value'); //HH:mm:ss\n        const maxTime = $('.maxtime').attr('value'); //HH:mm:ss\n        let calendarHeaders = {\n            left: 'dayGridMonth,timeGridWeek,listMonth',\n            center: 'title',\n            right: 'today,prev,next',\n        };\n        if (employeeSlotsFcData.length === 0) {\n            // There are no event to display. This is probably an empty slot sent for assignment\n            calendarHeaders = {\n                left: false,\n                center: 'title',\n                right: false,\n            };\n        }\n        const titleFormat = { month: \"long\", year: \"numeric\" };\n        let noEventsContent = _t(\"You don't have any shifts planned yet.\")\n        const openSlotsIds = $('.open_slots_ids').attr('value');\n        if (openSlotsIds) {\n            noEventsContent = _t(\"You don't have any shifts planned yet. You can assign yourself some of the available open shifts.\")\n        }\n        this.calendar = new FullCalendar.Calendar(document.querySelector(\"#calendar_employee .o_calendar_widget\"), {\n            // Settings\n            locale: locale,\n            initialView: defaultView,\n            navLinks: true, // can click day/week names to navigate views\n            dayMaxEventRows: 3, // allow \"more\" link when too many events\n            titleFormat: titleFormat,\n            initialDate: defaultStart,\n            displayEventEnd: true,\n            height: 'auto',\n            eventDidMount: this.onEventDidMount,\n            eventTextColor: 'white',\n            eventOverlap: true,\n            eventTimeFormat: {\n                hour: 'numeric',\n                minute: '2-digit',\n                meridiem: 'long',\n                omitZeroMinute: true,\n            },\n            slotMinTime: minTime,\n            slotMaxTime: maxTime,\n            headerToolbar: calendarHeaders,\n            // Data\n            events: employeeSlotsFcData,\n            // Event Function is called when clicking on the event\n            eventClick: this.eventFunction.bind(this),\n            buttonText: {\n                today: _t(\"Today\"),\n                dayGridMonth: _t(\"Month\"),\n                timeGridWeek: _t(\"Week\"),\n                listMonth: _t(\"List\"),\n            },\n            noEventsContent: noEventsContent,\n        });\n        this.calendar.setOption('locale', locale);\n        this.calendar.render();\n    },\n    willStart: async function () {\n        await loadBundle(\"web.fullcalendar_lib\");\n    },\n    onEventDidMount: function (calRender) {\n        const eventContent = calRender.el.querySelectorAll('.fc-event-time, .fc-event-title');\n        if (calRender.view.type !== 'listMonth') {\n            calRender.el.classList.add('px-2', 'py-1');\n        }\n        if (calRender.view.type === 'dayGridMonth') {\n            for (let i = 0; i < eventContent.length; i++) {\n                eventContent[i].classList.add('d-block', 'text-truncate');\n            }\n        }\n        calRender.el.classList.add('cursor-pointer');\n        calRender.el.childNodes[0].classList.add('fw-bold');\n        const timeElement = document.createElement('span');\n        timeElement.classList.add('ps-1');\n        const allocatedHours = calRender.event.extendedProps.alloc_hours;\n        const hoursSpan = document.createElement('span');\n        hoursSpan.textContent = `(${allocatedHours})`;\n        timeElement.appendChild(hoursSpan);\n        const allocatedPercent = calRender.event.extendedProps.alloc_perc;\n        if (allocatedPercent != 100) {\n            const percentSpan = document.createElement('span');\n            percentSpan.classList.add('ps-1');\n            percentSpan.textContent = `(${allocatedPercent}%)`;\n            timeElement.appendChild(percentSpan);\n        }\n        calRender.el.querySelector('.fc-event-time')?.appendChild(timeElement);\n\n        if (calRender.event.extendedProps.request_to_switch && !calRender.event.extendedProps.allow_self_unassign) {\n            calRender.el.style.borderColor = 'rgb(255, 172, 0)';\n            calRender.el.style.borderWidth = '5px';\n            calRender.el.style.opacity = '0.7';\n        }\n    },\n    formatDateAsBackend: function (date) {\n        return DateTime.fromJSDate(date).toLocaleString({\n            ...DateTime.DATE_SHORT,\n            ...DateTime.TIME_24_SIMPLE,\n            weekday: \"short\",\n        });\n    },\n    eventFunction: function (calEvent) {\n        const planningToken = $('.planning_token').attr('value');\n        const employeeToken = $('.employee_token').attr('value');\n        let displayFooter = false;\n        $(\".modal-title\").text(calEvent.event.title);\n        $(\".modal-header\").css(\"background-color\", calEvent.event.backgroundColor);\n        if (calEvent.event.extendedProps.request_to_switch && !calEvent.event.extendedProps.allow_self_unassign) {\n            document.getElementById(\"switch-warning\").style.display = \"block\";\n            $(\".warning-text\").text(\"You requested to switch this shift. Other employees can now assign themselves to it.\");\n        } else {\n            document.getElementById(\"switch-warning\").style.display = \"none\";\n        }\n        $('.o_start_date').text(this.formatDateAsBackend(calEvent.event.start));\n        let textValue = this.formatDateAsBackend(calEvent.event.end);\n        if (calEvent.event.extendedProps.alloc_hours) {\n            textValue += ` (${calEvent.event.extendedProps.alloc_hours})`;\n        }\n        if (parseFloat(calEvent.event.extendedProps.alloc_perc) < 100) {\n            textValue += ` (${calEvent.event.extendedProps.alloc_perc}%)`;\n        }\n        $('.o_end_date').text(textValue);\n        if (calEvent.event.extendedProps.role) {\n            $(\"#role\").prev().css(\"display\", \"\");\n            $(\"#role\").text(calEvent.event.extendedProps.role);\n            $(\"#role\").css(\"display\", \"\");\n        } else {\n            $(\"#role\").prev().css(\"display\", \"none\");\n            $(\"#role\").css(\"display\", \"none\");\n        }\n        if (calEvent.event.extendedProps.note) {\n            $(\"#note\").prev().css(\"display\", \"\");\n            $(\"#note\").text(calEvent.event.extendedProps.note);\n            $(\"#note\").css(\"display\", \"\");\n        } else {\n            $(\"#note\").prev().css(\"display\", \"none\");\n            $(\"#note\").css(\"display\", \"none\");\n        }\n        $(\"#allow_self_unassign\").text(calEvent.event.extendedProps.allow_self_unassign);\n        if (\n            calEvent.event.extendedProps.allow_self_unassign\n            && !calEvent.event.extendedProps.is_unassign_deadline_passed\n            ) {\n            document.getElementById(\"dismiss_shift\").style.display = \"block\";\n            displayFooter = true;\n        } else {\n            document.getElementById(\"dismiss_shift\").style.display = \"none\";\n        }\n        if (\n            !calEvent.event.extendedProps.request_to_switch\n            && !calEvent.event.extendedProps.is_past\n            && !calEvent.event.extendedProps.allow_self_unassign\n            ) {\n            document.getElementById(\"switch_shift\").style.display = \"block\";\n            displayFooter = true;\n        } else {\n            document.getElementById(\"switch_shift\").style.display = \"none\";\n        }\n        if (\n            calEvent.event.extendedProps.request_to_switch\n            && !calEvent.event.extendedProps.allow_self_unassign\n            ) {\n            document.getElementById(\"cancel_switch\").style.display = \"block\";\n            displayFooter = true;\n        } else {\n            document.getElementById(\"cancel_switch\").style.display = \"none\";\n        }\n        $(\"#modal_action_dismiss_shift\").attr(\"action\", \"/planning/\" + planningToken + \"/\" + employeeToken + \"/unassign/\" + calEvent.event.extendedProps.slot_id);\n        $(\"#modal_action_switch_shift\").attr(\"action\", \"/planning/\" + planningToken + \"/\" + employeeToken + \"/switch/\" + calEvent.event.extendedProps.slot_id);\n        $(\"#modal_action_cancel_switch\").attr(\"action\", \"/planning/\" + planningToken + \"/\" + employeeToken + \"/cancel_switch/\" + calEvent.event.extendedProps.slot_id);\n        $(\"#fc-slot-onclick-modal\").modal(\"show\");\n        document.getElementsByClassName(\"modal-footer\")[0].style.display = displayFooter ? \"block\" : \"none\" ;\n    },\n});\n\n// Add client actions\nexport default publicWidget.registry.PlanningView;\n", "/** @odoo-module **/\n\nclass EventAdditionalTourSteps {\n\n    _get_website_event_steps() {\n        return [];\n    }\n\n}\n\nexport default EventAdditionalTourSteps;\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { stepUtils } from \"@web_tour/tour_service/tour_utils\";\n\nimport EventAdditionalTourSteps from \"@event/js/tours/event_steps\";\n\nimport { markup } from \"@odoo/owl\";\n\nregistry.category(\"web_tour.tours\").add('event_tour', {\n    url: '/odoo',\n    steps: () => [stepUtils.showAppsMenuItem(), {\n    isActive: [\"enterprise\"],\n    trigger: '.o_app[data-menu-xmlid=\"event.event_main_menu\"]',\n    content: markup(_t(\"Ready to <b>organize events</b> in a few minutes? Let's get started!\")),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n}, {\n    isActive: [\"community\"],\n    trigger: '.o_app[data-menu-xmlid=\"event.event_main_menu\"]',\n    content: markup(_t(\"Ready to <b>organize events</b> in a few minutes? Let's get started!\")),\n    run: \"click\",\n},\n{\n    trigger: \".o_event_kanban_view\",\n},\n{\n    trigger: '.o-kanban-button-new',\n    content: markup(_t(\"Let's create your first <b>event</b>.\")),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n}, {\n    trigger: '.o_event_form_view div[name=\"name\"] textarea',\n    content: markup(_t(\"This is the <b>name</b> your guests will see when registering.\")),\n    run: \"edit Odoo Experience 2020\",\n}, {\n    trigger: '.o_event_form_view div[name=\"date_begin\"]',\n    run: function () {\n        const el1 = this.anchor.querySelector('input[data-field=\"date_begin\"]');\n        el1.value = '09/30/2020 08:00:00';\n        el1.dispatchEvent(new Event(\"change\"));\n        const el2 = this.anchor.querySelector('input[data-field=\"date_end\"]');\n        el2.value = '10/02/2020 23:00:00';\n        el2.dispatchEvent(new Event(\"change\"));\n    },\n}, {\n    trigger: '.o_event_form_view input[data-field=\"date_begin\"]',\n    content: markup(_t(\"Open date range picker.<br/>Pick a Start and End date for your event.\")),\n    run: \"click\",\n}, {\n    content: _t(\"Apply change.\"),\n    trigger: '.o_datetime_picker .o_datetime_buttons .o_apply',\n    run: \"click\",\n}, {\n    trigger: '.o_event_form_view div[name=\"event_ticket_ids\"] .o_field_x2many_list_row_add a',\n    content: markup(_t(\"Ticket types allow you to distinguish your attendees. Let's <b>create</b> a new one.\")),\n    run: \"click\",\n}, stepUtils.autoExpandMoreButtons(),\n...new EventAdditionalTourSteps()._get_website_event_steps(), {\n    trigger: '.o_event_form_view div[name=\"stage_id\"]',\n    content: _t(\"Now that your event is ready, click here to move it to another stage.\"),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n},\n{\n    trigger: `.o_event_form_view div[name=\"stage_id\"]`,\n},\n{\n    trigger: 'ol.breadcrumb li.breadcrumb-item:first',\n    content: markup(_t(\"Use the <b>breadcrumbs</b> to go back to your kanban overview.\")),\n    tooltipPosition: 'bottom',\n    run: 'click',\n}].filter(Boolean)});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport EventAdditionalTourSteps from \"@event/js/tours/event_steps\";\n\nimport { markup } from \"@odoo/owl\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { insertSnippet } from '@website/js/tours/tour_utils';\n\npatch(EventAdditionalTourSteps.prototype, {\n\n    _get_website_event_steps() {\n        return [\n            ...super._get_website_event_steps(), {\n                trigger: '.o_event_form_view button[title=\"Unpublished\"]',\n                content: markup(_t(\"Use this <b>shortcut</b> to easily access your event web page.\")),\n                tooltipPosition: 'bottom',\n                run: \"click\",\n            }, {\n                trigger: '.o_edit_website_container a',\n                content: markup(_t(\"With the Edit button, you can <b>customize</b> the web page visitors will see when registering.\")),\n                tooltipPosition: 'bottom',\n                run: \"click\",\n            },\n            ...insertSnippet({\n                id: \"s_image_text\",\n                name: \"Image - Text\",\n                groupName: \"Content\",\n            }),\n            {\n                trigger: 'button[data-action=\"save\"]',\n                content: markup(_t(\"Don't forget to click <b>save</b> when you're done.\")),\n                tooltipPosition: 'bottom',\n                run: \"click\",\n            },\n            {\n                trigger: \":iframe body:not(.editor_enable) .o_wevent_event\",\n            },\n            {\n                trigger: '.o_menu_systray_item.o_website_publish_container a',\n                content: markup(_t(\"Looking great! Let's now <b>publish</b> this page so that it becomes <b>visible</b> on your website!\")),\n                tooltipPosition: 'bottom',\n                run: \"click\",\n            },\n            {\n                trigger: \":iframe .o_wevent_event\",\n            },\n            {\n                trigger: '.o_website_edit_in_backend > a',\n                content: _t(\"This shortcut will bring you right back to the event form.\"),\n                tooltipPosition: 'bottom',\n                run: \"click\",\n            }];\n    }\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\npublicWidget.registry.displayTimerWidget = publicWidget.Widget.extend({\n    selector: '.o_display_timer',\n\n    /**\n     * This widget allows to display a dom element at the end of a certain time laps.\n     * There are 2 timers available:\n     *   - The main-timer: display the DOM element (using the displayClass) at the end of this timer.\n     *   - The pre-timer: additional timer to display the main-timer. This pre-timer can be invisible or visible,\n     *                    depending of the startCountdownDisplay option. Once the pre-timer is over,\n                          the main-timer is displayed.\n     * @override\n     */\n    start: function () {\n        var self = this;\n        return this._super.apply(this, arguments).then(function () {\n            self.options = self.el.dataset;\n            self.preCountdownDisplay = self.options[\"preCountdownDisplay\"];\n            self.preCountdownTime = parseInt(self.options[\"preCountdownTime\"]);\n            self.preCountdownText = self.options[\"preCountdownText\"];\n\n            self.mainCountdownTime = parseInt(self.options[\"mainCountdownTime\"]);\n            self.mainCountdownText = self.options[\"mainCountdownText\"];\n            self.mainCountdownDisplay = self.options[\"mainCountdownDisplay\"];\n\n            self.displayClass = self.options[\"displayClass\"];\n\n            if (self.preCountdownDisplay === \"true\") {\n                self.el.parentElement.classList.remove(\"d-none\");\n            }\n\n            self._checkTimer();\n            self.interval = setInterval(function () { self._checkTimer(); }, 1000);\n        });\n    },\n\n    /**\n     * This method removes 1 second to the current timer (pre-timer or main-timer)\n     * and call the method to update the DOM, unless main-timer is over. In that last case,\n     * the DOM element to show is displayed.\n     *\n     * @private\n     */\n    _checkTimer: function () {\n        var now = new Date();\n\n        var remainingPreSeconds = this.preCountdownTime - (now.getTime()/1000);\n        if (remainingPreSeconds <= 1) {\n            this.el.querySelector(\".o_countdown_text\").textContent = this.mainCountdownText;\n            if (this.mainCountdownDisplay === \"true\") {\n                this.el.parentElement.classList.remove(\"d-none\");\n            }\n            var remainingMainSeconds = this.mainCountdownTime - (now.getTime()/1000);\n            if (remainingMainSeconds <= 1) {\n                clearInterval(this.interval);\n                document.querySelector(this.displayClass).classList.remove(\"d-none\");\n                this.el.parentElement.classList.add(\"d-none\");\n            } else {\n                this._updateCountdown(remainingMainSeconds);\n            }\n        } else {\n            this._updateCountdown(remainingPreSeconds);\n        }\n    },\n\n    /**\n     * This method update the DOM to display the remaining time.\n     * from seconds, the method extract the number of days, hours, minutes and seconds and\n     * override the different DOM elements values.\n     *\n     * @private\n     */\n    _updateCountdown: function (remainingTime) {\n        var remainingSeconds = remainingTime;\n        var days = Math.floor(remainingSeconds / 86400);\n\n        remainingSeconds = remainingSeconds % 86400;\n        var hours = Math.floor(remainingSeconds / 3600);\n\n        remainingSeconds = remainingSeconds % 3600;\n        var minutes = Math.floor(remainingSeconds / 60);\n\n        remainingSeconds = Math.floor(remainingSeconds % 60);\n\n        const daysEl = this.el.querySelector(\"span.o_timer_days\");\n        if (daysEl) {\n            daysEl.textContent = days;\n        }\n        const hoursEl = this.el.querySelector(\"span.o_timer_hours\");\n        if (hoursEl) {\n            hoursEl.textContent = this._zeroPad(hours, 2);\n        }\n        const minutesEl = this.el.querySelector(\"span.o_timer_minutes\");\n        if (minutesEl) {\n            minutesEl.textContent = this._zeroPad(minutes, 2);\n        }\n        const secondsEl = this.el.querySelector(\"span.o_timer_seconds\");\n        if (secondsEl) {\n            secondsEl.textContent = this._zeroPad(remainingSeconds, 2);\n        }\n    },\n\n    /**\n     * Small tool to add leading z\u00e9ros to the given number, in function of the needed number of leading z\u00e9ros.\n     *\n     * @private\n     */\n    _zeroPad: function (num, places) {\n      var zero = places - num.toString().length + 1;\n      return new Array(+(zero > 0 && zero)).join(\"0\") + num;\n    },\n\n});\n\nexport default publicWidget.registry.countdownWidget;\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\npublicWidget.registry.RegisterToasterWidget = publicWidget.Widget.extend({\n    selector: '.o_wevent_register_toaster',\n    /**\n     * @override\n     */\n    init() {\n        this._super(...arguments);\n        this.notification = this.bindService(\"notification\");\n    },\n    /**\n     * This widget allows to display a toast message on the page.\n     *\n     * @override\n     */\n    start: function () {\n        const message = this.el.dataset.message;\n        if (message && message.length) {\n            this.notification.add(message, {\n                title: _t(\"Register\"),\n                type: 'info',\n            });\n        }\n        return this._super.apply(this, arguments);\n    },\n});\n\nexport default publicWidget.registry.RegisterToasterWidget;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { rpc } from \"@web/core/network/rpc\";\n\n// Catch registration form event, because of JS for attendee details\nvar EventRegistrationForm = publicWidget.Widget.extend({\n\n    /**\n     * @override\n     */\n    start: function () {\n        var self = this;\n        const post = this._getPost();\n        const noTicketsOrdered = Object.values(post).map((value) => parseInt(value)).every(value => value === 0);\n        var res = this._super.apply(this.arguments).then(function () {\n            self.__onClick = self._onClick.bind(self);\n            self.submitButtonEl = document.querySelector(\"#registration_form .a-submit\");\n            self.submitButtonEl.addEventListener(\"click\", self.__onClick);\n            self.submitButtonEl.disabled = noTicketsOrdered;\n        });\n        return res;\n    },\n\n    destroy() {\n        this.submitButtonEl.removeEventListener(\"click\", this.__onClick);\n        this._super(...arguments);\n    },\n\n    _getPost: function () {\n        var post = {};\n        const selectEls = document.querySelectorAll(\"#registration_form select\");\n        selectEls.forEach(function (selectEl) {\n            post[selectEl.name] = selectEl.value;\n        });\n        return post;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onClick(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        const formEl = ev.currentTarget.closest(\"form\");\n        const buttonEl = ev.currentTarget.closest(\"[type='submit']\");\n        const post = this._getPost();\n        buttonEl.disabled = true;\n        return rpc(formEl.action, post).then((modal) => {\n            const modalEl = new DOMParser().parseFromString(modal, \"text/html\").body.firstChild;\n            const _onClick = () => {\n                buttonEl.disabled = false;\n                modalEl.querySelector(\".js_goto_event\").removeEventListener(\"click\", _onClick);\n                modalEl.querySelector(\".btn-close\").removeEventListener(\"click\", _onClick);\n                modalEl.remove();\n            };\n            modalEl.querySelector(\".js_goto_event\").addEventListener(\"click\", _onClick);\n            modalEl.querySelector(\".btn-close\").addEventListener(\"click\", _onClick);\n            const formModal = Modal.getOrCreateInstance(modalEl, {\n                backdrop: \"static\",\n                keyboard: false,\n            });\n            formModal.show();\n        });\n    },\n});\n\npublicWidget.registry.EventRegistrationFormInstance = publicWidget.Widget.extend({\n    selector: '#registration_form',\n\n    /**\n     * @override\n     */\n    start: function () {\n        var def = this._super.apply(this, arguments);\n        this.instance = new EventRegistrationForm(this);\n        return Promise.all([def, this.instance.attachTo(this.el)]);\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        this.instance.setElement(null);\n        this._super.apply(this, arguments);\n        this.instance.setElement(this.el);\n    },\n});\n\npublicWidget.registry.EventPage = publicWidget.Widget.extend({\n    selector: '#o_wevent_event_submenu .dropdown-menu a.dropdown-toggle',\n    events: {\n        'click ': '_onClickSubDropDown',\n    },\n    _onClickSubDropDown:function(ev){\n        ev.stopPropagation()\n    }\n})\n\nexport default EventRegistrationForm;\n", "/** @odoo-module **/\n    import publicWidget from \"@web/legacy/js/public/public_widget\";\n\n    publicWidget.registry.ticketDetailsWidget = publicWidget.Widget.extend({\n        selector: '.o_wevent_js_ticket_details',\n        events: {\n            'change .form-select': '_onTicketQuantityChange'\n        },\n        start: function (){\n            this.foldedByDefault = parseInt(this.el.dataset.foldedByDefault) === 1;\n            return this._super.apply(this, arguments);\n        },\n\n        //--------------------------------------------------------------------------\n        // Private\n        //--------------------------------------------------------------------------\n\n        /**\n         * @private\n         */\n        _getTotalTicketCount: function (){\n            var ticketCount = 0;\n            const selectEls = this.el.querySelectorAll(\".form-select\");\n            selectEls.forEach(function (selectEl) {\n                ticketCount += parseInt(selectEl.value);\n            });\n            return ticketCount;\n        },\n\n        //--------------------------------------------------------------------------\n        // Handlers\n        //--------------------------------------------------------------------------\n        /**\n         * @private\n         */\n        _onTicketQuantityChange: function (){\n            const ticketQuantityChangeBtnEl = this.el.querySelector(\"button.btn-primary\");\n            ticketQuantityChangeBtnEl.disabled = this._getTotalTicketCount() === 0;\n        }\n    });\n\nexport default publicWidget.registry.ticketDetailsWidget;\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { getDataURLFromFile } from \"@web/core/utils/urls\";\nimport { checkFileSize } from \"@web/core/utils/files\";\n\nimport { Component, useRef, useState } from \"@odoo/owl\";\n\nexport class FileUploader extends Component {\n    static template = \"web.FileUploader\";\n    static props = {\n        onClick: { type: Function, optional: true },\n        onUploaded: Function,\n        onUploadComplete: { type: Function, optional: true },\n        multiUpload: { type: Boolean, optional: true },\n        inputName: { type: String, optional: true },\n        fileUploadClass: { type: String, optional: true },\n        acceptedFileExtensions: { type: String, optional: true },\n        slots: { type: Object, optional: true },\n        showUploadingText: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        showUploadingText: true,\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.fileInputRef = useRef(\"fileInput\");\n        this.state = useState({\n            isUploading: false,\n        });\n    }\n\n    /**\n     * @param {Event} ev\n     */\n    async onFileChange(ev) {\n        if (!ev.target.files.length) {\n            return;\n        }\n        const { target } = ev;\n        for (const file of ev.target.files) {\n            if (!checkFileSize(file.size, this.notification)) {\n                return null;\n            }\n            this.state.isUploading = true;\n            const data = await getDataURLFromFile(file);\n            if (!file.size) {\n                console.warn(`Error while uploading file : ${file.name}`);\n                this.notification.add(_t(\"There was a problem while uploading your file.\"), {\n                    type: \"danger\",\n                });\n            }\n            try {\n                await this.props.onUploaded({\n                    name: file.name,\n                    size: file.size,\n                    type: file.type,\n                    data: data.split(\",\")[1],\n                    objectUrl: file.type === \"application/pdf\" ? URL.createObjectURL(file) : null,\n                });\n            } finally {\n                this.state.isUploading = false;\n            }\n        }\n        target.value = null;\n        if (this.props.multiUpload && this.props.onUploadComplete) {\n            this.props.onUploadComplete({});\n        }\n    }\n\n    async onSelectFileButtonClick(ev) {\n        if (this.props.onClick) {\n            const ok = await this.props.onClick(ev);\n            if (ok !== undefined && !ok) {\n                return;\n            }\n        }\n        this.fileInputRef.el.click();\n    }\n}\n", "import { formatDate as _formatDate, formatDateTime as _formatDateTime } from \"@web/core/l10n/dates\";\nimport { localization as l10n } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { isBinarySize } from \"@web/core/utils/binary\";\nimport {\n    formatFloat as formatFloatNumber,\n    humanNumber,\n    insertThousandsSep,\n} from \"@web/core/utils/numbers\";\nimport { escape, exprToBoolean } from \"@web/core/utils/strings\";\n\nimport { markup } from \"@odoo/owl\";\nimport { formatCurrency } from \"@web/core/currency\";\n\n// -----------------------------------------------------------------------------\n// Helpers\n// -----------------------------------------------------------------------------\n\nfunction humanSize(value) {\n    if (!value) {\n        return \"\";\n    }\n    const suffix = value < 1024 ? \" \" + _t(\"Bytes\") : \"b\";\n    return (\n        humanNumber(value, {\n            decimals: 2,\n        }) + suffix\n    );\n}\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\n/**\n * @param {string} [value] base64 representation of the binary\n * @returns {string}\n */\nexport function formatBinary(value) {\n    if (!isBinarySize(value)) {\n        // Computing approximate size out of base64 encoded string\n        // http://en.wikipedia.org/wiki/Base64#MIME\n        return humanSize(value.length / 1.37);\n    }\n    // already bin_size\n    return value;\n}\n\n/**\n * @param {boolean} value\n * @returns {string}\n */\nexport function formatBoolean(value) {\n    return markup(`\n        <div class=\"o-checkbox d-inline-block me-2\">\n            <input id=\"boolean_checkbox\" type=\"checkbox\" class=\"form-check-input\" disabled ${\n                value ? \"checked\" : \"\"\n            }/>\n            <label for=\"boolean_checkbox\" class=\"form-check-label\"/>\n        </div>`);\n}\n\n/**\n * @param {string} value\n * @param {Object} [options] additional options\n * @param {boolean} [options.escape=false] if true, escapes the formatted value\n * @param {boolean} [options.isPassword=false] if true, returns '********'\n *   instead of the formatted value\n * @returns {string}\n */\nexport function formatChar(value, options) {\n    if (options && options.isPassword) {\n        return \"*\".repeat(value ? value.length : 0);\n    }\n    if (options && options.escape) {\n        value = escape(value);\n    }\n    return value;\n}\nformatChar.extractOptions = ({ attrs }) => {\n    return {\n        isPassword: exprToBoolean(attrs.password),\n    };\n};\n\nexport function formatDate(value, options) {\n    return _formatDate(value, options);\n}\nformatDate.extractOptions = ({ options }) => {\n    return { condensed: options.condensed };\n};\n\nexport function formatDateTime(value, options = {}) {\n    if (options.showTime === false) {\n        return _formatDate(value, options);\n    }\n    return _formatDateTime(value, options);\n}\nformatDateTime.extractOptions = ({ attrs, options }) => {\n    return {\n        ...formatDate.extractOptions({ attrs, options }),\n        showSeconds: exprToBoolean(options.show_seconds ?? true),\n        showTime: exprToBoolean(options.show_time ?? true),\n    };\n};\n\n/**\n * Returns a string representing a float.  The result takes into account the\n * user settings (to display the correct decimal separator).\n *\n * @param {number | false} value the value that should be formatted\n * @param {Object} [options]\n * @param {number[]} [options.digits] the number of digits that should be used,\n *   instead of the default digits precision in the field.\n * @param {boolean} [options.humanReadable] if true, large numbers are formatted\n *   to a human readable format.\n * @param {string} [options.decimalPoint] decimal separating character\n * @param {string} [options.thousandsSep] thousands separator to insert\n * @param {number[]} [options.grouping] array of relative offsets at which to\n *   insert `thousandsSep`. See `insertThousandsSep` method.\n * @param {number} [options.decimals] used for humanNumber formmatter\n * @param {boolean} [options.trailingZeros=true] if false, the decimal part\n *   won't contain unnecessary trailing zeros.\n * @returns {string}\n */\nexport function formatFloat(value, options = {}) {\n    if (value === false) {\n        return \"\";\n    }\n    if (!options.digits && options.field) {\n        options.digits = options.field.digits;\n    }\n    return formatFloatNumber(value, options);\n}\nformatFloat.extractOptions = ({ attrs, options }) => {\n    // Sadly, digits param was available as an option and an attr.\n    // The option version could be removed with some xml refactoring.\n    let digits;\n    if (attrs.digits) {\n        digits = JSON.parse(attrs.digits);\n    } else if (options.digits) {\n        digits = options.digits;\n    }\n    const humanReadable = !!options.human_readable;\n    const decimals = options.decimals || 0;\n    return { decimals, digits, humanReadable };\n};\n\n/**\n * Returns a string representing a float value, from a float converted with a\n * factor.\n *\n * @param {number | false} value\n * @param {Object} [options]\n * @param {number} [options.factor=1.0] conversion factor\n * @returns {string}\n */\nexport function formatFloatFactor(value, options = {}) {\n    if (value === false) {\n        return \"\";\n    }\n    const factor = options.factor || 1;\n    if (!options.digits && options.field) {\n        options.digits = options.field.digits;\n    }\n    return formatFloatNumber(value * factor, options);\n}\nformatFloatFactor.extractOptions = ({ attrs, options }) => {\n    return {\n        ...formatFloat.extractOptions({ attrs, options }),\n        factor: options.factor,\n    };\n};\n\n/**\n * Returns a string representing a time value, from a float.  The idea is that\n * we sometimes want to display something like 1:45 instead of 1.75, or 0:15\n * instead of 0.25.\n *\n * @param {number | false} value\n * @param {Object} [options]\n * @param {boolean} [options.noLeadingZeroHour] if true, format like 1:30 otherwise, format like 01:30\n * @param {boolean} [options.displaySeconds] if true, format like ?1:30:00 otherwise, format like ?1:30\n * @returns {string}\n */\nexport function formatFloatTime(value, options = {}) {\n    if (value === false) {\n        return \"\";\n    }\n    const isNegative = value < 0;\n    value = Math.abs(value);\n\n    let hour = Math.floor(value);\n    const milliSecLeft = Math.round(value * 3600000) - hour * 3600000;\n    // Although looking quite overkill, the following lines ensures that we do\n    // not have float issues while still considering that 59s is 00:00.\n    let min = milliSecLeft / 60000;\n    if (options.displaySeconds) {\n        min = Math.floor(min);\n    } else {\n        min = Math.round(min);\n    }\n    if (min === 60) {\n        min = 0;\n        hour = hour + 1;\n    }\n    min = String(min).padStart(2, \"0\");\n    if (!options.noLeadingZeroHour) {\n        hour = String(hour).padStart(2, \"0\");\n    }\n    let sec = \"\";\n    if (options.displaySeconds) {\n        sec = \":\" + String(Math.floor((milliSecLeft % 60000) / 1000)).padStart(2, \"0\");\n    }\n    return `${isNegative ? \"-\" : \"\"}${hour}:${min}${sec}`;\n}\nformatFloatTime.extractOptions = ({ options }) => {\n    return {\n        displaySeconds: options.displaySeconds,\n    };\n};\n\n/**\n * Returns a string representing an integer.  If the value is false, then we\n * return an empty string.\n *\n * @param {number | false | null} value\n * @param {Object} [options]\n * @param {boolean} [options.humanReadable] if true, large numbers are formatted\n *   to a human readable format.\n * @param {boolean} [options.isPassword=false] if returns true, acts like\n * @param {string} [options.thousandsSep] thousands separator to insert\n * @param {number[]} [options.grouping] array of relative offsets at which to\n * @param {number} [options.decimals] used for humanNumber formmatter\n *   insert `thousandsSep`. See `insertThousandsSep` method.\n * @returns {string}\n */\nexport function formatInteger(value, options = {}) {\n    if (value === false || value === null) {\n        return \"\";\n    }\n    if (options.isPassword) {\n        return \"*\".repeat(value.length);\n    }\n    if (options.humanReadable) {\n        return humanNumber(value, options);\n    }\n    const grouping = options.grouping || l10n.grouping;\n    const thousandsSep = \"thousandsSep\" in options ? options.thousandsSep : l10n.thousandsSep;\n    return insertThousandsSep(value.toFixed(0), thousandsSep, grouping);\n}\nformatInteger.extractOptions = ({ attrs, options }) => {\n    return {\n        decimals: options.decimals || 0,\n        humanReadable: !!options.human_readable,\n        isPassword: exprToBoolean(attrs.password),\n    };\n};\n\n/**\n * Returns a string representing a many2one value. The value is expected to be\n * either `false` or an array in the form [id, display_name]. The returned\n * value will then be the display name of the given value, or an empty string\n * if the value is false.\n *\n * @param {[number, string] | false} value\n * @param {Object} [options] additional options\n * @param {boolean} [options.escape=false] if true, escapes the formatted value\n * @returns {string}\n */\nexport function formatMany2one(value, options) {\n    if (!value) {\n        value = \"\";\n    } else if (value[1]) {\n        value = value[1];\n    } else {\n        value = _t(\"Unnamed\");\n    }\n    if (options && options.escape) {\n        value = encodeURIComponent(value);\n    }\n    return value;\n}\n\n/**\n * Returns a string representing a one2many or many2many value. The value is\n * expected to be either `false` or an array of ids. The returned value will\n * then be the count of ids in the given value in the form \"x record(s)\".\n *\n * @param {number[] | false} value\n * @returns {string}\n */\nexport function formatX2many(value) {\n    const count = value.currentIds.length;\n    if (count === 0) {\n        return _t(\"No records\");\n    } else if (count === 1) {\n        return _t(\"1 record\");\n    } else {\n        return _t(\"%s records\", count);\n    }\n}\n\n/**\n * Returns a string representing a monetary value. The result takes into account\n * the user settings (to display the correct decimal separator, currency, ...).\n *\n * @param {number | false} value the value that should be formatted\n * @param {Object} [options]\n *   additional options to override the values in the python description of the\n *   field.\n * @param {number} [options.currencyId] the id of the 'res.currency' to use\n * @param {string} [options.currencyField] the name of the field whose value is\n *   the currency id (ignored if options.currency_id).\n *   Note: if not given it will default to the field \"currency_field\" value or\n *   on \"currency_id\".\n * @param {Object} [options.data] a mapping of field names to field values,\n *   required with options.currencyField\n * @param {boolean} [options.noSymbol] this currency has not a sympbol\n * @param {boolean} [options.humanReadable] if true, large numbers are formatted\n *   to a human readable format.\n * @param {[number, number]} [options.digits] the number of digits that should\n *   be used, instead of the default digits precision in the field.  The first\n *   number is always ignored (legacy constraint)\n * @returns {string}\n */\nexport function formatMonetary(value, options = {}) {\n    // Monetary fields want to display nothing when the value is unset.\n    // You wouldn't want a value of 0 euro if nothing has been provided.\n    if (value === false) {\n        return \"\";\n    }\n\n    let currencyId = options.currencyId;\n    if (!currencyId && options.data) {\n        const currencyField =\n            options.currencyField ||\n            (options.field && options.field.currency_field) ||\n            \"currency_id\";\n        const dataValue = options.data[currencyField];\n        currencyId = Array.isArray(dataValue) ? dataValue[0] : dataValue;\n    }\n    return formatCurrency(value, currencyId, options);\n}\nformatMonetary.extractOptions = ({ options }) => {\n    return {\n        noSymbol: options.no_symbol,\n        currencyField: options.currency_field,\n    };\n};\n\n/**\n * Returns a string representing the given value (multiplied by 100)\n * concatenated with '%'.\n *\n * @param {number | false} value\n * @param {Object} [options]\n * @param {boolean} [options.noSymbol] if true, doesn't concatenate with \"%\"\n * @returns {string}\n */\nexport function formatPercentage(value, options = {}) {\n    value = value || 0;\n    options = Object.assign({ trailingZeros: false, thousandsSep: \"\" }, options);\n    if (!options.digits && options.field) {\n        options.digits = options.field.digits;\n    }\n    const formatted = formatFloatNumber(value * 100, options);\n    return `${formatted}${options.noSymbol ? \"\" : \"%\"}`;\n}\nformatPercentage.extractOptions = formatFloat.extractOptions;\n\n/**\n * Returns a string representing the value of the python properties field\n * or a properties definition field (see fields.py@Properties).\n *\n * @param {array|false} value\n * @param {Object} [field]\n *        a description of the field (note: this parameter is ignored)\n */\nfunction formatProperties(value, field) {\n    if (!value || !value.length) {\n        return \"\";\n    }\n    return value.map((property) => property[\"string\"]).join(\", \");\n}\n\n/**\n * Returns a string representing the value of the reference field.\n *\n * @param {Object|false} value Object with keys \"resId\" and \"displayName\"\n * @param {Object} [options={}]\n * @returns {string}\n */\nexport function formatReference(value, options) {\n    return formatMany2one(value ? [value.resId, value.displayName] : false, options);\n}\n\n/**\n * Returns a string representing the value of the many2one_reference field.\n *\n * @param {Object|false} value Object with keys \"resId\" and \"displayName\"\n * @returns {string}\n */\nexport function formatMany2oneReference(value) {\n    return value ? formatMany2one([value.resId, value.displayName]) : \"\";\n}\n\n/**\n * Returns a string of the value of the selection.\n *\n * @param {Object} [options={}]\n * @param {[string, string][]} [options.selection]\n * @param {Object} [options.field]\n * @returns {string}\n */\nexport function formatSelection(value, options = {}) {\n    const selection = options.selection || (options.field && options.field.selection) || [];\n    const option = selection.find((option) => option[0] === value);\n    return option ? option[1] : \"\";\n}\n\n/**\n * Returns the value or an empty string if it's falsy.\n *\n * @param {string | false} value\n * @returns {string}\n */\nexport function formatText(value) {\n    return value ? value.toString() : \"\";\n}\n\n/**\n * Returns the value.\n * Note that, this function is added to be coherent with the rest of the formatters.\n *\n * @param {html} value\n * @returns {html}\n */\nexport function formatHtml(value) {\n    return value;\n}\n\nexport function formatJson(value) {\n    return (value && JSON.stringify(value)) || \"\";\n}\n\nregistry\n    .category(\"formatters\")\n    .add(\"binary\", formatBinary)\n    .add(\"boolean\", formatBoolean)\n    .add(\"char\", formatChar)\n    .add(\"date\", formatDate)\n    .add(\"datetime\", formatDateTime)\n    .add(\"float\", formatFloat)\n    .add(\"float_factor\", formatFloatFactor)\n    .add(\"float_time\", formatFloatTime)\n    .add(\"html\", formatHtml)\n    .add(\"integer\", formatInteger)\n    .add(\"json\", formatJson)\n    .add(\"many2one\", formatMany2one)\n    .add(\"many2one_reference\", formatMany2oneReference)\n    .add(\"one2many\", formatX2many)\n    .add(\"many2many\", formatX2many)\n    .add(\"monetary\", formatMonetary)\n    .add(\"percentage\", formatPercentage)\n    .add(\"properties\", formatProperties)\n    .add(\"properties_definition\", formatProperties)\n    .add(\"reference\", formatReference)\n    .add(\"selection\", formatSelection)\n    .add(\"text\", formatText);\n", "export * from \"./store\";\nexport * from \"./record\";\nexport * from \"./make_store\";\nexport { AND, OR } from \"./misc\";\n", "import { markRaw, reactive, toRaw } from \"@odoo/owl\";\nimport { Store } from \"./store\";\nimport { STORE_SYM, isFieldDefinition, isMany, isRelation, modelRegistry } from \"./misc\";\nimport { Record } from \"./record\";\nimport { StoreInternal } from \"./store_internal\";\nimport { ModelInternal } from \"./model_internal\";\nimport { RecordInternal } from \"./record_internal\";\n\n/** @returns {import(\"models\").Store} */\nexport function makeStore(env, { localRegistry } = {}) {\n    const recordByLocalId = reactive(new Map());\n    // fake store for now, until it becomes a model\n    /** @type {import(\"models\").Store} */\n    Store.env = env;\n    let store = new Store();\n    store.env = env;\n    store.Model = Store;\n    store._ = markRaw(new StoreInternal());\n    store._raw = store;\n    store._proxyInternal = store;\n    store._proxy = store;\n    store.recordByLocalId = recordByLocalId;\n    Record.store = store;\n    /** @type {Object<string, typeof Record>} */\n    const Models = {};\n    const chosenModelRegistry = localRegistry ?? modelRegistry;\n    for (const [, _OgClass] of chosenModelRegistry.getEntries()) {\n        /** @type {typeof Record} */\n        const OgClass = _OgClass;\n        if (store[OgClass.getName()]) {\n            throw new Error(\n                `There must be no duplicated Model Names (duplicate found: ${OgClass.getName()})`\n            );\n        }\n        // classes cannot be made reactive because they are functions and they are not supported.\n        // work-around: make an object whose prototype is the class, so that static props become\n        // instance props.\n        /** @type {typeof Record} */\n        const Model = Object.create(OgClass);\n        // Produce another class with changed prototype, so that there are automatic get/set on relational fields\n        const Class = {\n            [OgClass.getName()]: class extends OgClass {\n                constructor() {\n                    super();\n                    this.setup();\n                    const record = this;\n                    record._raw = record;\n                    record.Model = Model;\n                    record._ = markRaw(\n                        record[STORE_SYM] ? new StoreInternal() : new RecordInternal()\n                    );\n                    const recordProxyInternal = new Proxy(record, {\n                        /**\n                         * @param {Record} record\n                         * @param {string} name\n                         * @param {Record} recordFullProxy\n                         */\n                        get(record, name, recordFullProxy) {\n                            recordFullProxy = record._.downgradeProxy(record, recordFullProxy);\n                            if (record._.gettingField || !Model._.fields.get(name)) {\n                                let res = Reflect.get(...arguments);\n                                if (typeof res === \"function\") {\n                                    res = res.bind(recordFullProxy);\n                                }\n                                return res;\n                            }\n                            if (Model._.fieldsCompute.get(name) && !Model._.fieldsEager.get(name)) {\n                                record._.fieldsComputeInNeed.set(name, true);\n                                if (record._.fieldsComputeOnNeed.get(name)) {\n                                    record._.compute(record, name);\n                                }\n                            }\n                            if (Model._.fieldsSort.get(name) && !Model._.fieldsEager.get(name)) {\n                                record._.fieldsSortInNeed.set(name, true);\n                                if (record._.fieldsSortOnNeed.get(name)) {\n                                    record._.sort(record, name);\n                                }\n                            }\n                            record._.gettingField = true;\n                            const val = recordFullProxy[name];\n                            record._.gettingField = false;\n                            if (isRelation(Model, name)) {\n                                const recordListFullProxy = val._proxy;\n                                if (isMany(Model, name)) {\n                                    return recordListFullProxy;\n                                }\n                                return recordListFullProxy[0];\n                            }\n                            return Reflect.get(record, name, recordFullProxy);\n                        },\n                        /**\n                         * @param {Record} record\n                         * @param {string} name\n                         */\n                        deleteProperty(record, name) {\n                            return store.MAKE_UPDATE(function recordDeleteProperty() {\n                                if (isRelation(Model, name)) {\n                                    const recordList = record[name];\n                                    recordList.clear();\n                                    return true;\n                                }\n                                return Reflect.deleteProperty(record, name);\n                            });\n                        },\n                        /**\n                         * Using record.update(data) is preferable for performance to batch process\n                         * when updating multiple fields at the same time.\n                         */\n                        set(record, name, val, receiver) {\n                            // ensure each field write goes through the updatingAttrs method exactly once\n                            if (record._.updatingAttrs.has(name)) {\n                                record[name] = val;\n                                return true;\n                            }\n                            return store.MAKE_UPDATE(function recordSet() {\n                                const reactiveSet = receiver !== record._proxyInternal;\n                                if (reactiveSet) {\n                                    record._.proxyUsed.set(name, true);\n                                }\n                                store._.updateFields(record, { [name]: val });\n                                if (reactiveSet) {\n                                    record._.proxyUsed.delete(name);\n                                }\n                                return true;\n                            });\n                        },\n                    });\n                    record._proxyInternal = recordProxyInternal;\n                    const recordProxy = reactive(recordProxyInternal);\n                    record._proxy = recordProxy;\n                    if (record?.[STORE_SYM]) {\n                        record.recordByLocalId = store.recordByLocalId;\n                        record._ = markRaw(toRaw(store._));\n                        store = record;\n                        Record.store = store;\n                    }\n                    for (const name of Model._.fields.keys()) {\n                        record._.prepareField(record, name, recordProxy);\n                    }\n                    return recordProxy;\n                }\n            },\n        }[OgClass.getName()];\n        Model._ = markRaw(new ModelInternal());\n        Object.assign(Model, {\n            Class,\n            env,\n            records: reactive({}),\n        });\n        Models[Model.getName()] = Model;\n        store[Model.getName()] = Model;\n        // Detect fields with a dummy record and setup getter/setters on them\n        const obj = new OgClass();\n        obj.setup();\n        for (const [name, val] of Object.entries(obj)) {\n            if (isFieldDefinition(val)) {\n                Model._.prepareField(name, val);\n            }\n        }\n    }\n    // Sync inverse fields\n    for (const Model of Object.values(Models)) {\n        for (const name of Model._.fields.keys()) {\n            if (!isRelation(Model, name)) {\n                continue;\n            }\n            const targetModel = Model._.fieldsTargetModel.get(name);\n            const inverse = Model._.fieldsInverse.get(name);\n            if (targetModel && !Models[targetModel]) {\n                throw new Error(`No target model ${targetModel} exists`);\n            }\n            if (inverse) {\n                const OtherModel = Models[targetModel];\n                const rel2TargetModel = OtherModel._.fieldsTargetModel.get(inverse);\n                const rel2Inverse = OtherModel._.fieldsInverse.get(inverse);\n                if (rel2TargetModel && rel2TargetModel !== Model.getName()) {\n                    throw new Error(\n                        `Fields ${Models[\n                            targetModel\n                        ].getName()}.${inverse} has wrong targetModel. Expected: \"${Model.getName()}\" Actual: \"${rel2TargetModel}\"`\n                    );\n                }\n                if (rel2Inverse && rel2Inverse !== name) {\n                    throw new Error(\n                        `Fields ${Models[\n                            targetModel\n                        ].getName()}.${inverse} has wrong inverse. Expected: \"${name}\" Actual: \"${rel2Inverse}\"`\n                    );\n                }\n                OtherModel._.fieldsTargetModel.set(inverse, Model.getName());\n                OtherModel._.fieldsInverse.set(inverse, name);\n                // // FIXME: lazy fields are not working properly with inverse.\n                Model._.fieldsEager.set(name, true);\n                OtherModel._.fieldsEager.set(inverse, true);\n            }\n        }\n    }\n    /**\n     * store/_rawStore are assigned on models at next step, but they are\n     * required on Store model to make the initial store insert.\n     */\n    Object.assign(store.Store, { store, _rawStore: store });\n    // Make true store (as a model)\n    store = toRaw(store.Store.insert())._raw;\n    for (const Model of Object.values(Models)) {\n        Model._rawStore = store;\n        Model.store = store._proxy;\n        store._proxy[Model.getName()] = Model;\n    }\n    Object.assign(store, { Models, storeReady: true });\n    return store._proxy;\n}\n", "import { markup } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\n/** @typedef {import(\"./record\").Record} Record */\n/** @typedef {import(\"./record_list\").RecordList} RecordList */\n\nexport const modelRegistry = registry.category(\"discuss.model\");\n\n/**\n * Class of markup, useful to detect content that is markup and to\n * automatically markup field during trusted insert\n */\nexport const Markup = markup(\"\").constructor;\n\nexport const FIELD_DEFINITION_SYM = Symbol(\"field_definition\");\n/** @typedef {ATTR_SYM|MANY_SYM|ONE_SYM} FIELD_SYM */\nexport const ATTR_SYM = Symbol(\"attr\");\nexport const MANY_SYM = Symbol(\"many\");\nexport const ONE_SYM = Symbol(\"one\");\nexport const OR_SYM = Symbol(\"or\");\nconst AND_SYM = Symbol(\"and\");\nexport const IS_RECORD_SYM = Symbol(\"isRecord\");\nexport const IS_FIELD_SYM = Symbol(\"isField\");\nexport const IS_DELETING_SYM = Symbol(\"isDeleting\");\nexport const IS_DELETED_SYM = Symbol(\"isDeleted\");\nexport const STORE_SYM = Symbol(\"store\");\n\nexport function AND(...args) {\n    return [AND_SYM, ...args];\n}\nexport function OR(...args) {\n    return [OR_SYM, ...args];\n}\n\nexport function isCommand(data) {\n    return [\"ADD\", \"DELETE\", \"ADD.noinv\", \"DELETE.noinv\"].includes(data?.[0]?.[0]);\n}\n/**\n * @param {typeof import(\"./record\").Record} Model\n * @param {string} fieldName\n */\nexport function isOne(Model, fieldName) {\n    return Model._.fieldsOne.get(fieldName);\n}\n/**\n * @param {typeof import(\"./record\").Record} Model\n * @param {string} fieldName\n */\nexport function isMany(Model, fieldName) {\n    return Model._.fieldsMany.get(fieldName);\n}\n/** @param {Record} record */\nexport function isRecord(record) {\n    return Boolean(record?._?.[IS_RECORD_SYM]);\n}\n/**\n * @param {typeof import(\"./record\").Record} Model\n * @param {string} fieldName\n */\nexport function isRelation(Model, fieldName) {\n    return isMany(Model, fieldName) || isOne(Model, fieldName);\n}\nexport function isFieldDefinition(val) {\n    return val?.[FIELD_DEFINITION_SYM];\n}\n", "import { ATTR_SYM, MANY_SYM, ONE_SYM } from \"./misc\";\n\nexport class ModelInternal {\n    /** @type {Map<string, boolean>} */\n    fields = new Map();\n    /** @type {Map<string, boolean>} */\n    fieldsAttr = new Map();\n    /** @type {Map<string, boolean>} */\n    fieldsOne = new Map();\n    /** @type {Map<string, boolean>} */\n    fieldsMany = new Map();\n    /** @type {Map<string, boolean>} */\n    fieldsHtml = new Map();\n    /** @type {Map<string, string>} */\n    fieldsTargetModel = new Map();\n    /** @type {Map<string, () => any>} */\n    fieldsCompute = new Map();\n    /** @type {Map<string, boolean>} */\n    fieldsEager = new Map();\n    /** @type {Map<string, string>} */\n    fieldsInverse = new Map();\n    /** @type {Map<string, () => void>} */\n    fieldsOnAdd = new Map();\n    /** @type {Map<string, () => void>} */\n    fieldsOnDelete = new Map();\n    /** @type {Map<string, () => void>} */\n    fieldsOnUpdate = new Map();\n    /** @type {Map<string, () => number>} */\n    fieldsSort = new Map();\n    /** @type {Map<string, string>} */\n    fieldsType = new Map();\n\n    prepareField(fieldName, data) {\n        this.fields.set(fieldName, true);\n        if (data[ATTR_SYM]) {\n            this.fieldsAttr.set(fieldName, true);\n        }\n        if (data[ONE_SYM]) {\n            this.fieldsOne.set(fieldName, true);\n        }\n        if (data[MANY_SYM]) {\n            this.fieldsMany.set(fieldName, true);\n        }\n        for (const key in data) {\n            const value = data[key];\n            switch (key) {\n                case \"html\": {\n                    if (!value) {\n                        break;\n                    }\n                    this.fieldsHtml.set(fieldName, value);\n                    break;\n                }\n                case \"targetModel\": {\n                    this.fieldsTargetModel.set(fieldName, value);\n                    break;\n                }\n                case \"compute\": {\n                    this.fieldsCompute.set(fieldName, value);\n                    break;\n                }\n                case \"eager\": {\n                    if (!value) {\n                        break;\n                    }\n                    this.fieldsEager.set(fieldName, value);\n                    break;\n                }\n                case \"sort\": {\n                    this.fieldsSort.set(fieldName, value);\n                    break;\n                }\n                case \"inverse\": {\n                    this.fieldsInverse.set(fieldName, value);\n                    break;\n                }\n                case \"onAdd\": {\n                    this.fieldsOnAdd.set(fieldName, value);\n                    break;\n                }\n                case \"onDelete\": {\n                    this.fieldsOnDelete.set(fieldName, value);\n                    break;\n                }\n                case \"onUpdate\": {\n                    this.fieldsOnUpdate.set(fieldName, value);\n                    break;\n                }\n                case \"type\": {\n                    this.fieldsType.set(fieldName, value);\n                    break;\n                }\n            }\n        }\n    }\n}\n", "import { toRaw } from \"@odoo/owl\";\nimport {\n    ATTR_SYM,\n    FIELD_DEFINITION_SYM,\n    IS_DELETED_SYM,\n    MANY_SYM,\n    ONE_SYM,\n    OR_SYM,\n    isCommand,\n    isMany,\n    isOne,\n    isRecord,\n    isRelation,\n    modelRegistry,\n} from \"./misc\";\nimport { serializeDate, serializeDateTime } from \"@web/core/l10n/dates\";\n\n/** @typedef {import(\"./misc\").FieldDefinition} FieldDefinition */\n/** @typedef {import(\"./misc\").RecordField} RecordField */\n/** @typedef {import(\"./record_list\").RecordList} RecordList */\n\nexport class Record {\n    /** @type {import(\"./model_internal\").ModelInternal} */\n    static _;\n    /** @type {import(\"./record_internal\").RecordInternal} */\n    _;\n    static id;\n    /** @type {import(\"@web/env\").OdooEnv} */\n    static env;\n    /** @type {import(\"@web/env\").OdooEnv} */\n    env;\n    /** @type {Object<string, Record>} */\n    static records;\n    /** @type {import(\"models\").Store} */\n    static store;\n    /** @param {() => any} fn */\n    static MAKE_UPDATE(fn) {\n        return this.store.MAKE_UPDATE(...arguments);\n    }\n    static onChange(record, name, cb) {\n        return this.store.onChange(...arguments);\n    }\n    static get(data) {\n        const Model = toRaw(this);\n        return this.records[Model.localId(data)];\n    }\n    static getName() {\n        return this._name || this.name;\n    }\n    static register(localRegistry) {\n        if (localRegistry) {\n            // Record-specific tests use local registry as to not affect other tests\n            localRegistry.add(this.getName(), this);\n        } else {\n            modelRegistry.add(this.getName(), this);\n        }\n    }\n    static localId(data) {\n        const Model = toRaw(this);\n        let idStr;\n        if (typeof data === \"object\" && data !== null) {\n            idStr = Model._localId(Model.id, data);\n        } else {\n            idStr = data; // non-object data => single id\n        }\n        return `${Model.getName()},${idStr}`;\n    }\n    static _localId(expr, data, { brackets = false } = {}) {\n        const Model = toRaw(this);\n        if (!Array.isArray(expr)) {\n            if (Model._.fields.get(expr)) {\n                if (Model._.fieldsMany.get(expr)) {\n                    throw new Error(\"Using a Record.Many() as id is not (yet) supported\");\n                }\n                if (!isRelation(Model, expr)) {\n                    return data[expr];\n                }\n                if (isCommand(data[expr])) {\n                    // Note: only Record.one() is supported\n                    const [cmd, data2] = data[expr].at(-1);\n                    if (cmd === \"DELETE\") {\n                        return undefined;\n                    } else {\n                        return `(${data2?.localId})`;\n                    }\n                }\n                // relational field (note: optional when OR)\n                if (isRecord(data[expr])) {\n                    return `(${data[expr]?.localId})`;\n                }\n                const TargetModelName = Model._.fieldsTargetModel.get(expr);\n                return `(${Model.store[TargetModelName].get(data[expr])?.localId})`;\n            }\n            return data[expr];\n        }\n        const vals = [];\n        for (let i = 1; i < expr.length; i++) {\n            vals.push(Model._localId(expr[i], data, { brackets: true }));\n        }\n        let res = vals.join(expr[0] === OR_SYM ? \" OR \" : \" AND \");\n        if (brackets) {\n            res = `(${res})`;\n        }\n        return res;\n    }\n    static _retrieveIdFromData(data) {\n        const Model = toRaw(this);\n        const res = {};\n        function _deepRetrieve(expr2) {\n            if (typeof expr2 === \"string\") {\n                if (isCommand(data[expr2])) {\n                    // Note: only Record.one() is supported\n                    const [cmd, data2] = data[expr2].at(-1);\n                    return Object.assign(res, {\n                        [expr2]:\n                            cmd === \"DELETE\"\n                                ? undefined\n                                : cmd === \"DELETE.noinv\"\n                                ? [[\"DELETE.noinv\", data2]]\n                                : cmd === \"ADD.noinv\"\n                                ? [[\"ADD.noinv\", data2]]\n                                : data2,\n                    });\n                }\n                return Object.assign(res, { [expr2]: data[expr2] });\n            }\n            if (expr2 instanceof Array) {\n                for (const expr of this.id) {\n                    if (typeof expr === \"symbol\") {\n                        continue;\n                    }\n                    _deepRetrieve(expr);\n                }\n            }\n        }\n        if (Model.id === undefined) {\n            return res;\n        }\n        if (typeof Model.id === \"string\") {\n            if (typeof data !== \"object\" || data === null) {\n                return { [Model.id]: data }; // non-object data => single id\n            }\n            if (isCommand(data[Model.id])) {\n                // Note: only Record.one() is supported\n                const [cmd, data2] = data[Model.id].at(-1);\n                return Object.assign(res, {\n                    [Model.id]:\n                        cmd === \"DELETE\"\n                            ? undefined\n                            : cmd === \"DELETE.noinv\"\n                            ? [[\"DELETE.noinv\", data2]]\n                            : cmd === \"ADD.noinv\"\n                            ? [[\"ADD.noinv\", data2]]\n                            : data2,\n                });\n            }\n            return { [Model.id]: data[Model.id] };\n        }\n        for (const expr of Model.id) {\n            if (typeof expr === \"symbol\") {\n                continue;\n            }\n            _deepRetrieve(expr);\n        }\n        return res;\n    }\n    /**\n     * Technical attribute, DO NOT USE in business code.\n     * This class is almost equivalent to current class of model,\n     * except this is a function, so we can new() it, whereas\n     * `this` is not, because it's an object.\n     * (in order to comply with OWL reactivity)\n     *\n     * @type {typeof Record}\n     */\n    static Class;\n    /**\n     * This method is almost equivalent to new Class, except that it properly\n     * setup relational fields of model with get/set, @see Class\n     *\n     * @returns {Record}\n     */\n    static new(data, ids) {\n        const Model = toRaw(this);\n        const store = Model._rawStore;\n        return store.MAKE_UPDATE(function RecordNew() {\n            const recordProxy = new Model.Class();\n            const record = toRaw(recordProxy)._raw;\n            Object.assign(record._, { localId: Model.localId(ids) });\n            Object.assign(recordProxy, { ...ids });\n            Model.records[record.localId] = recordProxy;\n            if (record.Model.getName() === \"Store\") {\n                Object.assign(record, {\n                    env: Model._rawStore.env,\n                    recordByLocalId: Model._rawStore.recordByLocalId,\n                });\n            }\n            Model._rawStore.recordByLocalId.set(record.localId, recordProxy);\n            for (const fieldName of record.Model._.fields.keys()) {\n                record._.requestCompute?.(record, fieldName);\n                record._.requestSort?.(record, fieldName);\n            }\n            return recordProxy;\n        });\n    }\n    /**\n     * @template {keyof import(\"models\").Models} M\n     * @param {M} targetModel\n     * @param {Object} [param1={}]\n     * @param {(this: Record) => any} [param1.compute] if set, the value of this relational field is declarative and\n     *   is computed automatically. All reactive accesses recalls that function. The context of\n     *   the function is the record. Returned value is new value assigned to this field.\n     * @param {boolean} [param1.eager=false] when field is computed, determines whether the computation\n     *   of this field is eager or lazy. By default, fields are computed lazily, which means that\n     *   they are computed when dependencies change AND when this field is being used. In eager mode,\n     *   the field is immediately (re-)computed when dependencies changes, which matches the built-in\n     *   behaviour of OWL reactive.\n     * @param {string} [param1.inverse] if set, the name of field in targetModel that acts as the inverse.\n     * @param {(this: Record, r: import(\"models\").Models[M]) => void} [param1.onAdd] function that is called when a record is added\n     *   in the relation.\n     * @param {(this: Record, r: import(\"models\").Models[M]) => void} [param1.onDelete] function that is called when a record is removed\n     *   from the relation.\n     * @param {(this: Record) => void} [param1.onUpdate] function that is called when the field value is updated.\n     *   This is called at least once at record creation.\n     * @returns {import(\"models\").Models[M]}\n     */\n    static one(targetModel, param1) {\n        return { ...param1, targetModel, [FIELD_DEFINITION_SYM]: true, [ONE_SYM]: true };\n    }\n    /**\n     * @template {keyof import(\"models\").Models} M\n     * @param {M} targetModel\n     * @param {Object} [param1={}]\n     * @param {(this: Record) => any} [param1.compute] if set, the value of this relational field is declarative and\n     *   is computed automatically. All reactive accesses recalls that function. The context of\n     *   the function is the record. Returned value is new value assigned to this field.\n     * @param {boolean} [param1.eager=false] when field is computed, determines whether the computation\n     *   of this field is eager or lazy. By default, fields are computed lazily, which means that\n     *   they are computed when dependencies change AND when this field is being used. In eager mode,\n     *   the field is immediately (re-)computed when dependencies changes, which matches the built-in\n     *   behaviour of OWL reactive.\n     * @param {string} [param1.inverse] if set, the name of field in targetModel that acts as the inverse.\n     * @param {(this: Record, r: import(\"models\").Models[M]) => void} [param1.onAdd] function that is called when a record is added\n     *   in the relation.\n     * @param {(this: Record, r: import(\"models\").Models[M]) => void} [param1.onDelete] function that is called when a record is removed\n     *   from the relation.\n     * @param {(this: Record) => void} [param1.onUpdate] function that is called when the field value is updated.\n     *   This is called at least once at record creation.\n     * @param {(this: Record, r1: import(\"models\").Models[M], r2: import(\"models\").Models[M]) => number} [param1.sort] if defined, this field\n     *   is automatically sorted by this function.\n     * @returns {import(\"models\").Models[M][]}\n     */\n    static many(targetModel, param1) {\n        return { ...param1, targetModel, [FIELD_DEFINITION_SYM]: true, [MANY_SYM]: true };\n    }\n    /**\n     * @template T\n     * @param {T} def\n     * @param {Object} [param1={}]\n     * @param {(this: Record) => any} [param1.compute] if set, the value of this attr field is declarative and\n     *   is computed automatically. All reactive accesses recalls that function. The context of\n     *   the function is the record. Returned value is new value assigned to this field.\n     * @param {boolean} [param1.eager=false] when field is computed, determines whether the computation\n     *   of this field is eager or lazy. By default, fields are computed lazily, which means that\n     *   they are computed when dependencies change AND when this field is being used. In eager mode,\n     *   the field is immediately (re-)computed when dependencies changes, which matches the built-in\n     *   behaviour of OWL reactive.\n     * @param {boolean} [param1.html] if set, the field value contains html value.\n     *   Useful to automatically markup when the insert is trusted.\n     * @param {(this: Record) => void} [param1.onUpdate] function that is called when the field value is updated.\n     *   This is called at least once at record creation.\n     * @param {(this: Record, Object, Object) => number} [param1.sort] if defined, this field is automatically sorted\n     *   by this function.\n     * @param {'datetime'|'date'} [param1.type] if defined, automatically transform to a\n     * specific type.\n     * @returns {T}\n     */\n    static attr(def, param1) {\n        return { ...param1, [FIELD_DEFINITION_SYM]: true, [ATTR_SYM]: true, default: def };\n    }\n    /** @returns {Record|Record[]} */\n    static insert(data, options = {}) {\n        const ModelFullProxy = this;\n        const Model = toRaw(ModelFullProxy);\n        const store = Model._rawStore;\n        return store.MAKE_UPDATE(function RecordInsert() {\n            const isMulti = Array.isArray(data);\n            if (!isMulti) {\n                data = [data];\n            }\n            const oldTrusted = store._.trusted;\n            store._.trusted = options.html ?? store._.trusted;\n            const res = data.map(function RecordInsertMap(d) {\n                return Model._insert.call(ModelFullProxy, d, options);\n            });\n            store._.trusted = oldTrusted;\n            if (!isMulti) {\n                return res[0];\n            }\n            return res;\n        });\n    }\n    /** @returns {Record} */\n    static _insert(data) {\n        const ModelFullProxy = this;\n        const Model = toRaw(ModelFullProxy);\n        const recordFullProxy = Model.preinsert.call(ModelFullProxy, data);\n        const record = toRaw(recordFullProxy)._raw;\n        record.update.call(record._proxy, data);\n        return recordFullProxy;\n    }\n    /** @returns {Record} */\n    static preinsert(data) {\n        const ModelFullProxy = this;\n        const Model = toRaw(ModelFullProxy);\n        const ids = Model._retrieveIdFromData(data);\n        for (const name in ids) {\n            if (\n                ids[name] &&\n                !isRecord(ids[name]) &&\n                !isCommand(ids[name]) &&\n                isRelation(Model, name)\n            ) {\n                // preinsert that record in relational field,\n                // as it is required to make current local id\n                ids[name] = Model._rawStore[Model._.fieldsTargetModel.get(name)].preinsert(\n                    ids[name]\n                );\n            }\n        }\n        return Model.get.call(ModelFullProxy, data) ?? Model.new(data, ids);\n    }\n\n    /** @returns {import(\"models\").Store} */\n    get store() {\n        return toRaw(this)._raw.Model._rawStore._proxy;\n    }\n    /** @returns {import(\"models\").Store} */\n    get _rawStore() {\n        return toRaw(this)._raw.Model._rawStore;\n    }\n    /**\n     * Technical attribute, contains the Model entry in the store.\n     * This is almost the same as the class, except it's an object\n     * (so it works with OWL reactivity), and it's the actual object\n     * that store the records.\n     *\n     * Indeed, `this.constructor.records` is there to initiate `records`\n     * on the store entry, but the class `static records` is not actually\n     * used because it's non-reactive, and we don't want to persistently\n     * store records on class, to make sure different tests do not share\n     * records.\n     *\n     * @type {typeof Record}\n     */\n    Model;\n    /** @type {string} */\n    get localId() {\n        return toRaw(this)._.localId;\n    }\n    /** @type {this} */\n    _raw;\n    /** @type {this} */\n    _proxyInternal;\n    /** @type {this} */\n    _proxy;\n\n    setup() {}\n\n    update(data) {\n        const record = toRaw(this)._raw;\n        const store = record._rawStore;\n        return store.MAKE_UPDATE(function recordUpdate() {\n            if (typeof data === \"object\" && data !== null) {\n                store._.updateFields(record, data);\n            } else {\n                // update on single-id data\n                store._.updateFields(record, { [record.Model.id]: data });\n            }\n        });\n    }\n\n    delete() {\n        const record = toRaw(this)._raw;\n        const store = record._rawStore;\n        return store.MAKE_UPDATE(function recordDelete() {\n            store._.ADD_QUEUE(\"delete\", record);\n        });\n    }\n\n    exists() {\n        return !this._[IS_DELETED_SYM];\n    }\n\n    /** @param {Record} record */\n    eq(record) {\n        return toRaw(this)._raw === toRaw(record)?._raw;\n    }\n\n    /** @param {Record} record */\n    notEq(record) {\n        return !this.eq(record);\n    }\n\n    /** @param {Record[]|RecordList} collection */\n    in(collection) {\n        if (!collection) {\n            return false;\n        }\n        return collection.some((record) => toRaw(record)._raw.eq(this));\n    }\n\n    /** @param {Record[]|RecordList} collection */\n    notIn(collection) {\n        return !this.in(collection);\n    }\n\n    toData() {\n        const recordProxy = this;\n        const record = toRaw(recordProxy)._raw;\n        const Model = record.Model;\n        const data = { ...recordProxy };\n        for (const name of Model._.fields.keys()) {\n            if (isMany(Model, name)) {\n                data[name] = record._proxyInternal[name].map((recordProxy) => {\n                    const record = toRaw(recordProxy)._raw;\n                    return record.toIdData.call(record._proxyInternal);\n                });\n            } else if (isOne(Model, name)) {\n                const otherRecord = toRaw(record._proxyInternal[name])?._raw;\n                data[name] = otherRecord?.toIdData.call(otherRecord._proxyInternal);\n            } else {\n                // Record.attr()\n                const value = recordProxy[name];\n                if (Model._.fieldsType.get(name) === \"datetime\" && value) {\n                    data[name] = serializeDateTime(value);\n                } else if (Model._.fieldsType.get(name) === \"date\" && value) {\n                    data[name] = serializeDate(value);\n                } else {\n                    data[name] = value;\n                }\n            }\n        }\n        delete data._;\n        delete data._fieldsValue;\n        delete data._proxy;\n        delete data._proxyInternal;\n        delete data._raw;\n        delete data.Model;\n        return data;\n    }\n    toIdData() {\n        const data = this.Model._retrieveIdFromData(this);\n        for (const [name, val] of Object.entries(data)) {\n            if (isRecord(val)) {\n                data[name] = val.toIdData();\n            }\n        }\n        return data;\n    }\n}\nRecord.register();\n", "/** @typedef {import(\"./record\").Record} Record */\n/** @typedef {import(\"./record_list\").RecordList} RecordList */\n\nimport { onChange } from \"@mail/utils/common/misc\";\nimport { IS_DELETED_SYM, IS_DELETING_SYM, IS_RECORD_SYM, isRelation } from \"./misc\";\nimport { RecordList } from \"./record_list\";\nimport { reactive, toRaw } from \"@odoo/owl\";\nimport { RecordUses } from \"./record_uses\";\n\nexport class RecordInternal {\n    [IS_RECORD_SYM] = true;\n    [IS_DELETED_SYM] = false;\n    // Note: state of fields in Maps rather than object is intentional for improved performance.\n    /**\n     * For computed field, determines whether the field is computing its value.\n     *\n     * @type {Map<string, boolean>}\n     */\n    fieldsComputing = new Map();\n    /**\n     * On lazy-sorted field, determines whether the field should be (re-)sorted\n     * when it's needed (i.e. accessed). Eager sorted fields are immediately re-sorted at end of update cycle,\n     * whereas lazy sorted fields wait extra for them being needed.\n     *\n     * @type {Map<string, boolean>}\n     */\n    fieldsSortOnNeed = new Map();\n    /**\n     * On lazy sorted-fields, determines whether this field is needed (i.e. accessed).\n     *\n     * @type {Map<string, boolean>}\n     */\n    fieldsSortInNeed = new Map();\n    /**\n     * For sorted field, determines whether the field is sorting its value.\n     *\n     * @type {Map<string, boolean>}\n     */\n    fieldsSorting = new Map();\n    /**\n     * On lazy computed-fields, determines whether this field is needed (i.e. accessed).\n     *\n     * @type {Map<string, boolean>}\n     */\n    fieldsComputeInNeed = new Map();\n    /**\n     * on lazy-computed field, determines whether the field should be (re-)computed\n     * when it's needed (i.e. accessed). Eager computed fields are immediately re-computed at end of update cycle,\n     * whereas lazy computed fields wait extra for them being needed.\n     *\n     * @type {Map<string, boolean>}\n     */\n    fieldsComputeOnNeed = new Map();\n    /** @type {Map<string, () => void>} */\n    fieldsOnUpdateObserves = new Map();\n    /** @type {Map<string, this>} */\n    fieldsSortProxy2 = new Map();\n    /** @type {Map<string, this>} */\n    fieldsComputeProxy2 = new Map();\n    uses = new RecordUses();\n    updatingAttrs = new Map();\n    proxyUsed = new Map();\n    /** @type {string} */\n    localId;\n    gettingField = false;\n\n    /**\n     * @param {Record} record\n     * @param {string} fieldName\n     * @param {Record} recordProxy\n     */\n    prepareField(record, fieldName, recordProxy) {\n        const self = this;\n        const Model = toRaw(record).Model;\n        if (isRelation(Model, fieldName)) {\n            // Relational fields contain symbols for detection in original class.\n            // This constructor is called on genuine records:\n            // - 'one' fields => undefined\n            // - 'many' fields => RecordList\n            // record[name]?.[0] is ONE_SYM or MANY_SYM\n            const recordList = new RecordList();\n            Object.assign(recordList._, {\n                name: fieldName,\n                owner: record,\n            });\n            Object.assign(recordList, {\n                _raw: recordList,\n                _store: record.store,\n            });\n            record[fieldName] = recordList;\n        } else {\n            record[fieldName] = record[fieldName].default;\n        }\n        if (Model._.fieldsCompute.get(fieldName)) {\n            if (!Model._.fieldsEager.get(fieldName)) {\n                onChange(recordProxy, fieldName, () => {\n                    if (this.fieldsComputing.get(fieldName)) {\n                        /**\n                         * Use a reactive to reset the computeInNeed flag when there is\n                         * a change. This assumes when other reactive are still\n                         * observing the value, its own callback will reset the flag to\n                         * true through the proxy getters.\n                         */\n                        this.fieldsComputeInNeed.delete(fieldName);\n                    }\n                });\n                // reset flags triggered by registering onChange\n                this.fieldsComputeInNeed.delete(fieldName);\n                this.fieldsSortInNeed.delete(fieldName);\n            }\n            const cb = function computeObserver() {\n                self.requestCompute(record, fieldName);\n            };\n            const computeProxy2 = reactive(recordProxy, cb);\n            this.fieldsComputeProxy2.set(fieldName, computeProxy2);\n        }\n        if (Model._.fieldsSort.get(fieldName)) {\n            if (!Model._.fieldsEager.get(fieldName)) {\n                onChange(recordProxy, fieldName, () => {\n                    if (this.fieldsSorting.get(fieldName)) {\n                        /**\n                         * Use a reactive to reset the inNeed flag when there is a\n                         * change. This assumes if another reactive is still observing\n                         * the value, its own callback will reset the flag to true\n                         * through the proxy getters.\n                         */\n                        this.fieldsSortInNeed.delete(fieldName);\n                    }\n                });\n                // reset flags triggered by registering onChange\n                this.fieldsComputeInNeed.delete(fieldName);\n                this.fieldsSortInNeed.delete(fieldName);\n            }\n            const sortProxy2 = reactive(recordProxy, function sortObserver() {\n                self.requestSort(record, fieldName);\n            });\n            this.fieldsSortProxy2.set(fieldName, sortProxy2);\n        }\n        if (Model._.fieldsOnUpdate.get(fieldName)) {\n            const store = Model.store;\n            store._onChange(recordProxy, fieldName, (obs) => {\n                this.fieldsOnUpdateObserves.set(fieldName, obs);\n                if (store._.UPDATE !== 0) {\n                    store._.ADD_QUEUE(\"onUpdate\", record, fieldName);\n                } else {\n                    this.onUpdate(record, fieldName);\n                }\n            });\n        }\n    }\n\n    requestCompute(record, fieldName, { force = false } = {}) {\n        if (record._[IS_DELETING_SYM]) {\n            return;\n        }\n        const Model = record.Model;\n        if (!Model._.fieldsCompute.get(fieldName)) {\n            return;\n        }\n        const store = record._rawStore;\n        if (store._.UPDATE !== 0 && !force) {\n            store._.ADD_QUEUE(\"compute\", record, fieldName);\n        } else {\n            if (Model._.fieldsEager.get(fieldName) || this.fieldsComputeInNeed.get(fieldName)) {\n                this.compute(record, fieldName);\n            } else {\n                this.fieldsComputeOnNeed.set(fieldName, true);\n            }\n        }\n    }\n    requestSort(record, fieldName, { force } = {}) {\n        if (record._[IS_DELETING_SYM]) {\n            return;\n        }\n        const Model = record.Model;\n        if (!Model._.fieldsSort.get(fieldName)) {\n            return;\n        }\n        const store = record._rawStore;\n        if (store._.UPDATE !== 0 && !force) {\n            store._.ADD_QUEUE(\"sort\", record, fieldName);\n        } else {\n            if (Model._.fieldsEager.get(fieldName) || this.fieldsSortInNeed.get(fieldName)) {\n                this.sort(record, fieldName);\n            } else {\n                this.fieldsSortOnNeed.set(fieldName, true);\n            }\n        }\n    }\n    /**\n     * @param {Record} record\n     * @param {string} fieldName\n     */\n    compute(record, fieldName) {\n        const Model = record.Model;\n        const store = record._rawStore;\n        this.fieldsComputing.set(fieldName, true);\n        this.fieldsComputeOnNeed.delete(fieldName);\n        store._.updateFields(record, {\n            [fieldName]: Model._.fieldsCompute\n                .get(fieldName)\n                .call(this.fieldsComputeProxy2.get(fieldName)),\n        });\n        this.fieldsComputing.delete(fieldName);\n    }\n    /**\n     * @param {Record} record\n     * @param {string} fieldName\n     */\n    sort(record, fieldName) {\n        const Model = record.Model;\n        if (!Model._.fieldsSort.get(fieldName)) {\n            return;\n        }\n        const store = record._rawStore;\n        this.fieldsSortOnNeed.delete(fieldName);\n        this.fieldsSorting.set(fieldName, true);\n        const proxy2Sort = this.fieldsSortProxy2.get(fieldName);\n        const func = Model._.fieldsSort.get(fieldName).bind(proxy2Sort);\n        if (isRelation(Model, fieldName)) {\n            store._.sortRecordList(proxy2Sort[fieldName]._proxy, func);\n        } else {\n            // sort on copy of list so that reactive observers not triggered while sorting\n            const copy = [...proxy2Sort[fieldName]];\n            copy.sort(func);\n            const hasChanged = copy.some((item, index) => item !== record[fieldName][index]);\n            if (hasChanged) {\n                proxy2Sort[fieldName] = copy;\n            }\n        }\n        this.fieldsSorting.delete(fieldName);\n    }\n    onUpdate(record, fieldName) {\n        const Model = record.Model;\n        if (!Model._.fieldsOnUpdate.get(fieldName)) {\n            return;\n        }\n        /**\n         * Forward internal proxy for performance as onUpdate does not\n         * need reactive (observe is called separately).\n         */\n        Model._.fieldsOnUpdate.get(fieldName).call(record._proxyInternal);\n        this.fieldsOnUpdateObserves.get(fieldName)?.();\n    }\n    /**\n     * The internal reactive is only necessary to trigger outer reactives when\n     * writing on it. As it has no callback, reading through it has no effect,\n     * except slowing down performance and complexifying the stack.\n     */\n    downgradeProxy(record, fullProxy) {\n        return record._proxy === fullProxy ? record._proxyInternal : fullProxy;\n    }\n}\n", "import { markRaw, reactive, toRaw } from \"@odoo/owl\";\nimport { isRecord } from \"./misc\";\n\n/** @param {RecordList} reclist */\nfunction getInverse(reclist) {\n    return reclist._.owner.Model._.fieldsInverse.get(reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction getTargetModel(reclist) {\n    return reclist._.owner.Model._.fieldsTargetModel.get(reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction isComputeField(reclist) {\n    return reclist._.owner.Model._.fieldsCompute.get(reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction isSortField(reclist) {\n    return reclist._.owner.Model._.fieldsSort.get(reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction isEager(reclist) {\n    return reclist._.owner.Model._.fieldsEager.get(reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction setComputeInNeed(reclist) {\n    reclist._.owner._.fieldsComputeInNeed.set(reclist._.name, true);\n}\n\n/** @param {RecordList} reclist */\nfunction setSortInNeed(reclist) {\n    reclist._.owner._.fieldsSortInNeed.set(reclist._.name, true);\n}\n\n/** @param {RecordList} reclist */\nfunction isComputeOnNeed(reclist) {\n    return reclist._.owner._.fieldsComputeOnNeed.get(reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction isSortOnNeed(reclist) {\n    return reclist._.owner._.fieldsSortOnNeed.get(reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction computeField(reclist) {\n    reclist._.owner._.compute(reclist._.owner, reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction sortField(reclist) {\n    reclist._.owner._.sort(reclist._.owner, reclist._.name);\n}\n\n/** @param {RecordList} reclist */\nfunction isOne(reclist) {\n    return reclist._.owner.Model._.fieldsOne.get(reclist._.name);\n}\n\nexport class RecordListInternal {\n    /** @type {string} */\n    name;\n    /** @type {Record} */\n    owner;\n\n    /**\n     * Version of add() that does not update the inverse.\n     * This is internally called when inserting (with intent to add)\n     * on relational field with inverse, to prevent infinite loops.\n     *\n     * @param {RecordList} recordList\n     * @param {...Record}\n     */\n    addNoinv(recordList, ...records) {\n        const self = this;\n        const store = recordList._store;\n        if (isOne(recordList)) {\n            const last = records.at(-1);\n            if (isRecord(last) && last.in(recordList)) {\n                return;\n            }\n            const record = self.insert(\n                recordList,\n                last,\n                function recordList_AddNoInvOneInsert(record) {\n                    if (record.localId !== recordList.data[0]) {\n                        const old = recordList._proxy.at(-1);\n                        recordList._proxy.data.pop();\n                        old?._.uses.delete(recordList);\n                        recordList._proxy.data.push(record.localId);\n                        self.syncLength(recordList);\n                        record._.uses.add(recordList);\n                    }\n                },\n                { inv: false }\n            );\n            store._.ADD_QUEUE(\"onAdd\", self.owner, self.name, record);\n            return;\n        }\n        for (const val of records) {\n            if (isRecord(val) && val.in(recordList)) {\n                continue;\n            }\n            const record = self.insert(\n                recordList,\n                val,\n                function recordList_AddNoInvManyInsert(record) {\n                    if (recordList.data.indexOf(record.localId) === -1) {\n                        recordList._proxy.data.push(record.localId);\n                        self.syncLength(recordList);\n                        record._.uses.add(recordList);\n                    }\n                },\n                { inv: false }\n            );\n            store._.ADD_QUEUE(\"onAdd\", self.owner, self.name, record);\n        }\n    }\n    /** @param {R[]|any[]} data */\n    assign(recordList, data) {\n        const self = this;\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListAssign() {\n            /** @type {Record[]|Set<Record>|RecordList<Record|any[]>} */\n            const collection = isRecord(data) ? [data] : data;\n            // data and collection could be same record list,\n            // save before clear to not push mutated recordlist that is empty\n            const vals = [...collection];\n            const oldRecords = recordList._proxyInternal.slice\n                .call(recordList._proxy)\n                .map((recordProxy) => toRaw(recordProxy)._raw);\n            const newRecords = vals.map((val) =>\n                self.insert(recordList, val, function recordListAssignInsert(record) {\n                    if (record.notIn(oldRecords)) {\n                        record._.uses.add(recordList);\n                        store._.ADD_QUEUE(\"onAdd\", self.owner, self.name, record);\n                    }\n                })\n            );\n            const inverse = getInverse(recordList);\n            for (const oldRecord of oldRecords) {\n                if (oldRecord.notIn(newRecords)) {\n                    oldRecord._.uses.delete(recordList);\n                    store._.ADD_QUEUE(\"onDelete\", self.owner, self.name, oldRecord);\n                    if (inverse) {\n                        oldRecord[inverse].delete(self.owner);\n                    }\n                }\n            }\n            recordList._proxy.data = newRecords.map((newRecord) => newRecord.localId);\n            recordList._.syncLength(recordList);\n        });\n    }\n    /**\n     * Version of delete() that does not update the inverse.\n     * This is internally called when inserting (with intent to delete)\n     * on relational field with inverse, to prevent infinite loops.\n     *\n     * @param {RecordList} recordList\n     * @param {...Record}\n     */\n    deleteNoinv(recordList, ...records) {\n        const self = this;\n        const store = recordList._store;\n        for (const val of records) {\n            const record = this.insert(\n                recordList,\n                val,\n                function recordList_DeleteNoInv_Insert(record) {\n                    const index = recordList.data.indexOf(record.localId);\n                    if (index !== -1) {\n                        const old = recordList._proxy.at(-1);\n                        recordList.splice.call(recordList._proxy, index, 1);\n                        self.syncLength(recordList);\n                        old._.uses.delete(recordList);\n                    }\n                },\n                { inv: false }\n            );\n            store._.ADD_QUEUE(\"onDelete\", self.owner, self.name, record);\n        }\n    }\n    /**\n     * The internal reactive is only necessary to trigger outer reactives when\n     * writing on it. As it has no callback, reading through it has no effect,\n     * except slowing down performance and complexifying the stack.\n     *\n     * @param {RecordList} recordList\n     * @param {RecordList} fullProxy\n     */\n    downgradeProxy(recordList, fullProxy) {\n        return recordList._proxy === fullProxy ? recordList._proxyInternal : fullProxy;\n    }\n    /**\n     * @param {RecordList} recordList\n     * @param {R|any} val\n     * @param {(R) => void} [fn] function that is called in-between preinsert and\n     *   insert. Preinsert only inserted what's needed to make record, while\n     *   insert finalize with all remaining data.\n     * @param {boolean} [inv=true] whether the inverse should be added or not.\n     *   It is always added except when during an insert on a relational field,\n     *   in order to avoid infinite loop.\n     * @param {\"ADD\"|\"DELETE} [mode=\"ADD\"] the mode of insert on the relation.\n     *   Important to match the inverse. Most of the time it's \"ADD\", that is when\n     *   inserting the relation the inverse should be added. Exception when the insert\n     *   comes from deletion, we want to \"DELETE\".\n     */\n    insert(recordList, val, fn, { inv = true, mode = \"ADD\" } = {}) {\n        const inverse = getInverse(recordList);\n        const targetModel = getTargetModel(recordList);\n        if (typeof val !== \"object\") {\n            // single-id data\n            val = { [recordList._store[targetModel].id]: val };\n        }\n        if (inverse && inv) {\n            // special command to call addNoinv/deleteNoInv, to prevent infinite loop\n            const target = isRecord(val) && val._raw === val ? val._proxy : val;\n            target[inverse] = [[mode === \"ADD\" ? \"ADD.noinv\" : \"DELETE.noinv\", recordList._.owner]];\n        }\n        /** @type {R} */\n        let newRecordProxy;\n        if (!isRecord(val)) {\n            newRecordProxy = recordList._store[targetModel].preinsert(val);\n        } else {\n            newRecordProxy = val;\n        }\n        const newRecord = toRaw(newRecordProxy)._raw;\n        fn?.(newRecord);\n        if (!isRecord(val)) {\n            // was preinserted, fully insert now\n            recordList._store[targetModel].insert(val);\n        }\n        return newRecord;\n    }\n    /**\n     * Sync reclist.data length with array length, as to not introduce confusion while debugging\n     *\n     * @param {RecordList} reclist\n     */\n    syncLength(reclist) {\n        reclist.length = reclist.data.length;\n    }\n}\n\n/** * @template {Record} R */\nexport class RecordList extends Array {\n    /** @type {import(\"models\").Store} */\n    _store;\n    /** @type {string[]} */\n    data = [];\n    /** @type {this} */\n    _raw;\n    /** @type {this} */\n    _proxyInternal;\n    /** @type {this} */\n    _proxy;\n    _ = markRaw(new RecordListInternal());\n\n    constructor() {\n        super();\n        const recordList = this;\n        recordList._raw = recordList;\n        const recordListProxyInternal = new Proxy(recordList, {\n            /** @param {RecordList<R>} receiver */\n            get(recordList, name, recordListFullProxy) {\n                recordListFullProxy = recordList._.downgradeProxy(recordList, recordListFullProxy);\n                if (\n                    typeof name === \"symbol\" ||\n                    Object.keys(recordList).includes(name) ||\n                    Object.prototype.hasOwnProperty.call(recordList.constructor.prototype, name)\n                ) {\n                    let res = Reflect.get(...arguments);\n                    if (typeof res === \"function\") {\n                        res = res.bind(recordListFullProxy);\n                    }\n                    return res;\n                }\n                if (isComputeField(recordList) && !isEager(recordList)) {\n                    setComputeInNeed(recordList);\n                    if (isComputeOnNeed(recordList)) {\n                        computeField(recordList);\n                    }\n                }\n                if (name === \"length\") {\n                    return recordListFullProxy.data.length;\n                }\n                if (isSortField(recordList) && !isEager(recordList)) {\n                    setSortInNeed(recordList);\n                    if (isSortOnNeed(recordList)) {\n                        sortField(recordList);\n                    }\n                }\n                if (typeof name !== \"symbol\" && !window.isNaN(parseInt(name))) {\n                    // support for \"array[index]\" syntax\n                    const index = parseInt(name);\n                    return recordListFullProxy._store.recordByLocalId.get(\n                        recordListFullProxy.data[index]\n                    );\n                }\n                // Attempt an unimplemented array method call\n                const array = [...recordList[Symbol.iterator].call(recordListFullProxy)];\n                return array[name]?.bind(array);\n            },\n            /** @param {RecordList<R>} recordListProxy */\n            set(recordList, name, val, recordListProxy) {\n                const store = recordList._store;\n                return store.MAKE_UPDATE(function recordListSet() {\n                    if (typeof name !== \"symbol\" && !window.isNaN(parseInt(name))) {\n                        // support for \"array[index] = r3\" syntax\n                        const index = parseInt(name);\n                        recordList._.insert(\n                            recordList,\n                            val,\n                            function recordListSet_Insert(newRecord) {\n                                const oldRecord = toRaw(recordList._store.recordByLocalId).get(\n                                    recordList.data[index]\n                                );\n                                if (oldRecord && oldRecord.notEq(newRecord)) {\n                                    oldRecord._.uses.delete(recordList);\n                                }\n                                store._.ADD_QUEUE(\n                                    \"onDelete\",\n                                    recordList._.owner,\n                                    recordList._.name,\n                                    oldRecord\n                                );\n                                const inverse = getInverse(recordList);\n                                if (inverse) {\n                                    oldRecord[inverse].delete(recordList);\n                                }\n                                recordListProxy.data[index] = newRecord?.localId;\n                                if (newRecord) {\n                                    newRecord._.uses.add(recordList);\n                                    store._.ADD_QUEUE(\n                                        \"onAdd\",\n                                        recordList._.owner,\n                                        recordList._.name,\n                                        newRecord\n                                    );\n                                    if (inverse) {\n                                        newRecord[inverse].add(recordList);\n                                    }\n                                }\n                            }\n                        );\n                    } else if (name === \"length\") {\n                        const newLength = parseInt(val);\n                        if (newLength !== recordList.data.length) {\n                            if (newLength < recordList.data.length) {\n                                recordList.splice.call(\n                                    recordListProxy,\n                                    newLength,\n                                    recordList.length - newLength\n                                );\n                            }\n                            recordListProxy.data.length = newLength;\n                            recordList._.syncLength(recordList);\n                        }\n                    } else {\n                        return Reflect.set(recordList, name, val, recordListProxy);\n                    }\n                    return true;\n                });\n            },\n        });\n        recordList._proxyInternal = recordListProxyInternal;\n        recordList._proxy = reactive(recordListProxyInternal);\n        return recordList;\n    }\n    /** @param {R[]} records */\n    push(...records) {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListPush() {\n            for (const val of records) {\n                const record = recordList._.insert(\n                    recordList,\n                    val,\n                    function recordListPushInsert(record) {\n                        recordList._proxy.data.push(record.localId);\n                        recordList._.syncLength(recordList);\n                        record._.uses.add(recordList);\n                    }\n                );\n                store._.ADD_QUEUE(\"onAdd\", recordList._.owner, recordList._.name, record);\n                const inverse = getInverse(recordList);\n                if (inverse) {\n                    record[inverse].add(recordList._.owner);\n                }\n            }\n            return recordListFullProxy.data.length;\n        });\n    }\n    /** @returns {R} */\n    pop() {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListPop() {\n            /** @type {R} */\n            const oldRecordProxy = recordListFullProxy.at(-1);\n            if (oldRecordProxy) {\n                recordList.splice.call(recordListFullProxy, recordListFullProxy.length - 1, 1);\n            }\n            return oldRecordProxy;\n        });\n    }\n    /** @returns {R} */\n    shift() {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListShift() {\n            const recordProxy = recordListFullProxy._store.recordByLocalId.get(\n                recordListFullProxy.data.shift()\n            );\n            recordList._.syncLength(recordList);\n            if (!recordProxy) {\n                return;\n            }\n            const record = toRaw(recordProxy)._raw;\n            record._.uses.delete(recordList);\n            store._.ADD_QUEUE(\"onDelete\", recordList._.owner, recordList._.name, record);\n            const inverse = getInverse(recordList);\n            if (inverse) {\n                record[inverse].delete(recordList._.owner);\n            }\n            return recordProxy;\n        });\n    }\n    /** @param {R[]} records */\n    unshift(...records) {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListUnshift() {\n            for (let i = records.length - 1; i >= 0; i--) {\n                const record = recordList._.insert(recordList, records[i], (record) => {\n                    recordList._proxy.data.unshift(record.localId);\n                    recordList._.syncLength(recordList);\n                    record._.uses.add(recordList);\n                });\n                store._.ADD_QUEUE(\"onAdd\", recordList._.owner, recordList._.name, record);\n                const inverse = getInverse(recordList);\n                if (inverse) {\n                    record[inverse].add(recordList._.owner);\n                }\n            }\n            return recordListFullProxy.data.length;\n        });\n    }\n    /** @param {R} recordProxy */\n    indexOf(recordProxy) {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        return recordListFullProxy.data.indexOf(toRaw(recordProxy)?._raw.localId);\n    }\n    /**\n     * @param {number} [start]\n     * @param {number} [deleteCount]\n     * @param {...R} [newRecordsProxy]\n     */\n    splice(start, deleteCount, ...newRecordsProxy) {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListSplice() {\n            const oldRecordsProxy = recordList._proxyInternal.slice.call(\n                recordListFullProxy,\n                start,\n                start + deleteCount\n            );\n            const list = recordListFullProxy.data.slice(); // splice on copy of list so that reactive observers not triggered while splicing\n            list.splice(\n                start,\n                deleteCount,\n                ...newRecordsProxy.map((newRecordProxy) => toRaw(newRecordProxy)._raw.localId)\n            );\n            if (isOne(recordList) && start === 0 && deleteCount === 1) {\n                // avoid replacing whole list, to avoid triggering observers too much\n                if (list.length === 0) {\n                    recordList._proxy.data.pop();\n                } else {\n                    recordList._proxy.data[0] = list[0];\n                }\n            } else {\n                recordList._proxy.data = list;\n            }\n            recordList._.syncLength(recordList);\n            for (const oldRecordProxy of oldRecordsProxy) {\n                const oldRecord = toRaw(oldRecordProxy)._raw;\n                oldRecord._.uses.delete(recordList);\n                store._.ADD_QUEUE(\"onDelete\", recordList._.owner, recordList._.name, oldRecord);\n                const inverse = getInverse(recordList);\n                if (inverse) {\n                    oldRecord[inverse].delete(recordList._.owner);\n                }\n            }\n            for (const newRecordProxy of newRecordsProxy) {\n                const newRecord = toRaw(newRecordProxy)._raw;\n                newRecord._.uses.add(recordList);\n                store._.ADD_QUEUE(\"onAdd\", recordList._.owner, recordList._.name, newRecord);\n                const inverse = getInverse(recordList);\n                if (inverse) {\n                    newRecord[inverse].add(recordList._.owner);\n                }\n            }\n        });\n    }\n    /** @param {(a: R, b: R) => boolean} func */\n    sort(func) {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListSort() {\n            recordList._store._.sortRecordList(recordListFullProxy, func);\n            return recordListFullProxy;\n        });\n    }\n    /** @param {...R[]|...RecordList[R]} collections */\n    concat(...collections) {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        return recordListFullProxy.data\n            .map((localId) => recordListFullProxy._store.recordByLocalId.get(localId))\n            .concat(...collections.map((c) => [...c]));\n    }\n    /**\n     * @param {...R}\n     * @returns {R|R[]} the added record(s)\n     */\n    add(...records) {\n        const recordList = toRaw(this)._raw;\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListAdd() {\n            if (isOne(recordList)) {\n                const last = records.at(-1);\n                if (isRecord(last) && recordList.data.includes(toRaw(last)._raw.localId)) {\n                    return last;\n                }\n                return recordList._.insert(\n                    recordList,\n                    last,\n                    function recordListAddInsertOne(record) {\n                        if (record.localId !== recordList.data[0]) {\n                            recordList.splice.call(recordList._proxy, 0, 1, record);\n                        }\n                    }\n                );\n            }\n            const res = [];\n            for (const val of records) {\n                if (isRecord(val) && recordList.data.includes(val.localId)) {\n                    continue;\n                }\n                const rec = recordList._.insert(\n                    recordList,\n                    val,\n                    function recordListAddInsertMany(record) {\n                        if (recordList.data.indexOf(record.localId) === -1) {\n                            recordList.push.call(recordList._proxy, record);\n                        }\n                    }\n                );\n                res.push(rec);\n            }\n            return res.length === 1 ? res[0] : res;\n        });\n    }\n    /** @param {...R}  */\n    delete(...records) {\n        const recordList = toRaw(this)._raw;\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListDelete() {\n            for (const val of records) {\n                recordList._.insert(\n                    recordList,\n                    val,\n                    function recordListDelete_Insert(record) {\n                        const index = recordList.data.indexOf(record.localId);\n                        if (index !== -1) {\n                            recordList.splice.call(recordList._proxy, index, 1);\n                        }\n                    },\n                    { mode: \"DELETE\" }\n                );\n            }\n        });\n    }\n    clear() {\n        const recordList = toRaw(this)._raw;\n        const store = recordList._store;\n        return store.MAKE_UPDATE(function recordListClear() {\n            while (recordList.data.length > 0) {\n                recordList.pop.call(recordList._proxy);\n            }\n        });\n    }\n    /** @yields {R} */\n    *[Symbol.iterator]() {\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        for (const localId of recordListFullProxy.data) {\n            yield recordListFullProxy._store.recordByLocalId.get(localId);\n        }\n    }\n    /** @param {number} index */\n    at(index) {\n        // this custom implement of \"at\" is slightly faster than auto-calling unimplement array method\n        const recordList = toRaw(this)._raw;\n        const recordListFullProxy = recordList._.downgradeProxy(recordList, this);\n        return recordListFullProxy._store.recordByLocalId.get(recordListFullProxy.data.at(index));\n    }\n}\n", "export class RecordUses {\n    /**\n     * Track the uses of a record. Each record contains a single `RecordUses`:\n     * - Key: localId of record that uses current record\n     * - Value: Map where key is relational field name, and value is number\n     *          of time current record is present in this relation.\n     *\n     * @type {Map<string, Map<string, number>>}}\n     */\n    data = new Map();\n    /** @param {RecordList} list */\n    add(list) {\n        const record = list._.owner;\n        if (!this.data.has(record.localId)) {\n            this.data.set(record.localId, new Map());\n        }\n        const use = this.data.get(record.localId);\n        if (!use.get(list._.name)) {\n            use.set(list._.name, 0);\n        }\n        use.set(list._.name, use.get(list._.name) + 1);\n    }\n    /** @param {RecordList} list */\n    delete(list) {\n        const record = list._.owner;\n        if (!this.data.has(record.localId)) {\n            return;\n        }\n        const use = this.data.get(record.localId);\n        if (!use.get(list._.name)) {\n            return;\n        }\n        use.set(list._.name, use.get(list._.name) - 1);\n        if (use.get(list._.name) === 0) {\n            use.delete(list._.name);\n        }\n    }\n}\n", "import { Record } from \"./record\";\nimport { IS_DELETED_SYM, STORE_SYM } from \"./misc\";\nimport { reactive, toRaw } from \"@odoo/owl\";\n\n/** @typedef {import(\"./record_list\").RecordList} RecordList */\n\nexport class Store extends Record {\n    /** @type {import(\"./store_internal\").StoreInternal} */\n    _;\n    [STORE_SYM] = true;\n    /** @type {Map<string, Record>} */\n    recordByLocalId;\n    storeReady = false;\n    /**\n     * @param {string} localId\n     * @returns {Record}\n     */\n    get(localId) {\n        return this.recordByLocalId.get(localId);\n    }\n\n    /** @param {() => any} fn */\n    MAKE_UPDATE(fn) {\n        this._.UPDATE++;\n        const res = fn();\n        this._.UPDATE--;\n        if (this._.UPDATE === 0) {\n            // pretend an increased update cycle so that nothing in queue creates many small update cycles\n            this._.UPDATE++;\n            while (\n                this._.FC_QUEUE.size > 0 ||\n                this._.FS_QUEUE.size > 0 ||\n                this._.FA_QUEUE.size > 0 ||\n                this._.FD_QUEUE.size > 0 ||\n                this._.FU_QUEUE.size > 0 ||\n                this._.RO_QUEUE.size > 0 ||\n                this._.RD_QUEUE.size > 0 ||\n                this._.RHD_QUEUE.size > 0\n            ) {\n                const FC_QUEUE = new Map(this._.FC_QUEUE);\n                const FS_QUEUE = new Map(this._.FS_QUEUE);\n                const FA_QUEUE = new Map(this._.FA_QUEUE);\n                const FD_QUEUE = new Map(this._.FD_QUEUE);\n                const FU_QUEUE = new Map(this._.FU_QUEUE);\n                const RO_QUEUE = new Map(this._.RO_QUEUE);\n                const RD_QUEUE = new Map(this._.RD_QUEUE);\n                const RHD_QUEUE = new Map(this._.RHD_QUEUE);\n                this._.FC_QUEUE.clear();\n                this._.FS_QUEUE.clear();\n                this._.FA_QUEUE.clear();\n                this._.FD_QUEUE.clear();\n                this._.FU_QUEUE.clear();\n                this._.RO_QUEUE.clear();\n                this._.RD_QUEUE.clear();\n                this._.RHD_QUEUE.clear();\n                while (FC_QUEUE.size > 0) {\n                    /** @type {[Record, Map<string, true>]} */\n                    const [record, recMap] = FC_QUEUE.entries().next().value;\n                    FC_QUEUE.delete(record);\n                    for (const fieldName of recMap.keys()) {\n                        record._.requestCompute(record, fieldName, { force: true });\n                    }\n                }\n                while (FS_QUEUE.size > 0) {\n                    /** @type {[Record, Map<string, true>]} */\n                    const [record, recMap] = FS_QUEUE.entries().next().value;\n                    FS_QUEUE.delete(record);\n                    for (const fieldName of recMap.keys()) {\n                        record._.requestSort(record, fieldName, { force: true });\n                    }\n                }\n                while (FA_QUEUE.size > 0) {\n                    /** @type {[Record, Map<string, Map<Record, true>>]} */\n                    const [record, recMap] = FA_QUEUE.entries().next().value;\n                    FA_QUEUE.delete(record);\n                    while (recMap.size > 0) {\n                        /** @type {[string, Map<Record, true>]} */\n                        const [fieldName, fieldMap] = recMap.entries().next().value;\n                        recMap.delete(fieldName);\n                        const onAdd = record.Model._.fieldsOnAdd.get(fieldName);\n                        for (const addedRec of fieldMap.keys()) {\n                            onAdd?.call(record._proxy, addedRec._proxy);\n                        }\n                    }\n                }\n                while (FD_QUEUE.size > 0) {\n                    /** @type {[Record, Map<string, Map<Record, true>>]} */\n                    const [record, recMap] = FD_QUEUE.entries().next().value;\n                    FD_QUEUE.delete(record);\n                    while (recMap.size > 0) {\n                        /** @type {[string, Map<Record, true>]} */\n                        const [fieldName, fieldMap] = recMap.entries().next().value;\n                        recMap.delete(fieldName);\n                        const onDelete = record.Model._.fieldsOnDelete.get(fieldName);\n                        for (const removedRec of fieldMap.keys()) {\n                            onDelete?.call(record._proxy, removedRec._proxy);\n                        }\n                    }\n                }\n                while (FU_QUEUE.size > 0) {\n                    /** @type {[Record, Map<string, true>]} */\n                    const [record, map] = FU_QUEUE.entries().next().value;\n                    FU_QUEUE.delete(record);\n                    for (const fieldName of map.keys()) {\n                        record._.onUpdate(record, fieldName);\n                    }\n                }\n                while (RO_QUEUE.size > 0) {\n                    /** @type {Map<Function, true>} */\n                    const cb = RO_QUEUE.keys().next().value;\n                    RO_QUEUE.delete(cb);\n                    cb();\n                }\n                while (RD_QUEUE.size > 0) {\n                    /** @type {Record} */\n                    const record = RD_QUEUE.keys().next().value;\n                    RD_QUEUE.delete(record);\n                    for (const [localId, names] of record._.uses.data.entries()) {\n                        for (const [name2, count] of names.entries()) {\n                            const usingRecord2 = toRaw(this.recordByLocalId).get(localId);\n                            if (!usingRecord2) {\n                                // record already deleted, clean inverses\n                                record._.uses.data.delete(localId);\n                                continue;\n                            }\n                            if (usingRecord2.Model._.fieldsMany.get(name2)) {\n                                for (let c = 0; c < count; c++) {\n                                    usingRecord2[name2].delete(record);\n                                }\n                            } else {\n                                usingRecord2[name2] = undefined;\n                            }\n                        }\n                    }\n                    this._.ADD_QUEUE(\"hard_delete\", toRaw(record));\n                }\n                while (RHD_QUEUE.size > 0) {\n                    // effectively delete the record\n                    /** @type {Record} */\n                    const record = RHD_QUEUE.keys().next().value;\n                    RHD_QUEUE.delete(record);\n                    record._[IS_DELETED_SYM] = true;\n                    delete record.Model.records[record.localId];\n                    this.recordByLocalId.delete(record.localId);\n                }\n            }\n            this._.UPDATE--;\n        }\n        return res;\n    }\n    onChange(record, name, cb) {\n        return this._onChange(record, name, (observe) => {\n            const fn = () => {\n                observe();\n                cb();\n            };\n            if (this._.UPDATE !== 0) {\n                if (!this._.RO_QUEUE.has(fn)) {\n                    this._.RO_QUEUE.set(fn, true);\n                }\n            } else {\n                fn();\n            }\n        });\n    }\n    /**\n     * Version of onChange where the callback receives observe function as param.\n     * This is useful when there's desire to postpone calling the callback function,\n     * in which the observe is also intended to have its invocation postponed.\n     *\n     * @param {Record} record\n     * @param {string|string[]} key\n     * @param {(observe: Function) => any} callback\n     * @returns {function} function to call to stop observing changes\n     */\n    _onChange(record, key, callback) {\n        let proxy;\n        function _observe() {\n            // access proxy[key] only once to avoid triggering reactive get() many times\n            const val = proxy[key];\n            if (typeof val === \"object\" && val !== null) {\n                void Object.keys(val);\n            }\n            if (Array.isArray(val)) {\n                void val.length;\n                void toRaw(val).forEach.call(val, (i) => i);\n            }\n        }\n        if (Array.isArray(key)) {\n            for (const k of key) {\n                this._onChange(record, k, callback);\n            }\n            return;\n        }\n        let ready = true;\n        proxy = reactive(record, () => {\n            if (ready) {\n                callback(_observe);\n            }\n        });\n        _observe();\n        return () => {\n            ready = false;\n        };\n    }\n}\n", "/** @typedef {import(\"./record\").Record} Record */\n/** @typedef {import(\"./record_list\").RecordList} RecordList */\n\nimport { markup, toRaw } from \"@odoo/owl\";\nimport { RecordInternal } from \"./record_internal\";\nimport { deserializeDate, deserializeDateTime } from \"@web/core/l10n/dates\";\nimport { IS_DELETING_SYM, Markup, isCommand, isMany } from \"./misc\";\n\nexport class StoreInternal extends RecordInternal {\n    /**\n     * Determines whether the inserts are considered trusted or not.\n     * Useful to auto-markup html fields when this is set\n     */\n    trusted = false;\n    /** @type {Map<import(\"./record\").Record, Map<string, true>>} */\n    FC_QUEUE = new Map(); // field-computes\n    /** @type {Map<import(\"./record\").Record, Map<string, true>>} */\n    FS_QUEUE = new Map(); // field-sorts\n    /** @type {Map<import(\"./record\").Record, Map<string, Map<import(\"./record\").Record, true>>>} */\n    FA_QUEUE = new Map(); // field-onadds\n    /** @type {Map<import(\"./record\").Record, Map<string, Map<import(\"./record\").Record, true>>>} */\n    FD_QUEUE = new Map(); // field-ondeletes\n    /** @type {Map<import(\"./record\").Record, Map<string, true>>} */\n    FU_QUEUE = new Map(); // field-onupdates\n    /** @type {Map<Function, true>} */\n    RO_QUEUE = new Map(); // record-onchanges\n    /** @type {Map<Record, true>} */\n    RD_QUEUE = new Map(); // record-deletes\n    /** @type {Map<Record, true>} */\n    RHD_QUEUE = new Map(); // record-hard-deletes\n    UPDATE = 0;\n\n    /**\n     * @param {\"compute\"|\"sort\"|\"onAdd\"|\"onDelete\"|\"onUpdate\"|\"hard_delete\"} type\n     * @param {...any} params\n     */\n    ADD_QUEUE(type, ...params) {\n        switch (type) {\n            case \"delete\": {\n                /** @type {import(\"./record\").Record} */\n                const [record] = params;\n                if (!this.RD_QUEUE.has(record)) {\n                    this.RD_QUEUE.set(record, true);\n                }\n                break;\n            }\n            case \"compute\": {\n                /** @type {[import(\"./record\").Record, string]} */\n                const [record, fieldName] = params;\n                let recMap = this.FC_QUEUE.get(record);\n                if (!recMap) {\n                    recMap = new Map();\n                    this.FC_QUEUE.set(record, recMap);\n                }\n                recMap.set(fieldName, true);\n                break;\n            }\n            case \"sort\": {\n                /** @type {[import(\"./record\").Record, string]} */\n                const [record, fieldName] = params;\n                let recMap = this.FS_QUEUE.get(record);\n                if (!recMap) {\n                    recMap = new Map();\n                    this.FS_QUEUE.set(record, recMap);\n                }\n                recMap.set(fieldName, true);\n                break;\n            }\n            case \"onAdd\": {\n                /** @type {[import(\"./record\").Record, string, import(\"./record\").Record]} */\n                const [record, fieldName, addedRec] = params;\n                const Model = record.Model;\n                if (Model._.fieldsSort.get(fieldName)) {\n                    this.ADD_QUEUE(\"sort\", record, fieldName);\n                }\n                if (!Model._.fieldsOnAdd.get(fieldName)) {\n                    return;\n                }\n                let recMap = this.FA_QUEUE.get(record);\n                if (!recMap) {\n                    recMap = new Map();\n                    this.FA_QUEUE.set(record, recMap);\n                }\n                let fieldMap = recMap.get(fieldName);\n                if (!fieldMap) {\n                    fieldMap = new Map();\n                    recMap.set(fieldName, fieldMap);\n                }\n                fieldMap.set(addedRec, true);\n                break;\n            }\n            case \"onDelete\": {\n                /** @type {[import(\"./record\").Record, string, import(\"./record\").Record]} */\n                const [record, fieldName, removedRec] = params;\n                const Model = record.Model;\n                if (!Model._.fieldsOnDelete.get(fieldName)) {\n                    return;\n                }\n                let recMap = this.FD_QUEUE.get(record);\n                if (!recMap) {\n                    recMap = new Map();\n                    this.FD_QUEUE.set(record, recMap);\n                }\n                let fieldMap = recMap.get(fieldName);\n                if (!fieldMap) {\n                    fieldMap = new Map();\n                    recMap.set(fieldName, fieldMap);\n                }\n                fieldMap.set(removedRec, true);\n                break;\n            }\n            case \"onUpdate\": {\n                /** @type {[import(\"./record\").Record, string]} */\n                const [record, fieldName] = params;\n                let recMap = this.FU_QUEUE.get(record);\n                if (!recMap) {\n                    recMap = new Map();\n                    this.FU_QUEUE.set(record, recMap);\n                }\n                recMap.set(fieldName, true);\n                break;\n            }\n            case \"hard_delete\": {\n                /** @type {import(\"./record\").Record} */\n                const [record] = params;\n                record._[IS_DELETING_SYM] = true;\n                if (!this.RHD_QUEUE.has(record)) {\n                    this.RHD_QUEUE.set(record, true);\n                }\n                break;\n            }\n        }\n    }\n    /** @param {RecordList<Record>} recordListFullProxy */\n    sortRecordList(recordListFullProxy, func) {\n        const recordList = toRaw(recordListFullProxy)._raw;\n        // sort on copy of list so that reactive observers not triggered while sorting\n        const recordsFullProxy = recordListFullProxy.data.map((localId) =>\n            recordListFullProxy._store.recordByLocalId.get(localId)\n        );\n        recordsFullProxy.sort(func);\n        const data = recordsFullProxy.map((recordFullProxy) => toRaw(recordFullProxy)._raw.localId);\n        const hasChanged = recordList.data.some((localId, i) => localId !== data[i]);\n        if (hasChanged) {\n            recordListFullProxy.data = data;\n        }\n    }\n    /**\n     * @param {Record} record\n     * @param {string} fieldName\n     * @param {any} value\n     */\n    updateAttr(record, fieldName, value) {\n        const Model = record.Model;\n        const fieldType = Model._.fieldsType.get(fieldName);\n        const fieldHtml = Model._.fieldsHtml.get(fieldName);\n        // ensure each field write goes through the proxy exactly once to trigger reactives\n        const targetRecord = record._.proxyUsed.has(fieldName) ? record : record._proxy;\n        let shouldChange = record[fieldName] !== value;\n        if (fieldType === \"datetime\" && value) {\n            if (!(value instanceof luxon.DateTime)) {\n                value = deserializeDateTime(value);\n            }\n            shouldChange = !record[fieldName] || !value.equals(record[fieldName]);\n        }\n        if (fieldType === \"date\" && value) {\n            if (!(value instanceof luxon.DateTime)) {\n                value = deserializeDate(value);\n            }\n            shouldChange = !record[fieldName] || !value.equals(record[fieldName]);\n        }\n        let newValue = value;\n        if (fieldHtml && this.trusted) {\n            shouldChange =\n                record[fieldName]?.toString() !== value?.toString() ||\n                !(record[fieldName] instanceof Markup);\n            newValue = typeof value === \"string\" ? markup(value) : value;\n        }\n        if (shouldChange) {\n            record._.updatingAttrs.set(fieldName, true);\n            targetRecord[fieldName] = newValue;\n            record._.updatingAttrs.delete(fieldName);\n        }\n    }\n    /**\n     * @param {Record} record\n     * @param {Object} vals\n     */\n    updateFields(record, vals) {\n        for (const [fieldName, value] of Object.entries(vals)) {\n            if (!record.Model._.fields.get(fieldName) || record.Model._.fieldsAttr.get(fieldName)) {\n                this.updateAttr(record, fieldName, value);\n            } else {\n                this.updateRelation(record, fieldName, value);\n            }\n        }\n    }\n    /**\n     * @param {Record} record\n     * @param {string} fieldName\n     * @param {any} value\n     */\n    updateRelation(record, fieldName, value) {\n        /** @type {RecordList<Record>} */\n        const recordList = record[fieldName];\n        if (isMany(record.Model, fieldName)) {\n            this.updateRelationMany(recordList, value);\n        } else {\n            this.updateRelationOne(recordList, value);\n        }\n    }\n    /**\n     * @param {RecordList} recordList\n     * @param {any} value\n     */\n    updateRelationMany(recordList, value) {\n        if (isCommand(value)) {\n            for (const [cmd, cmdData] of value) {\n                if (Array.isArray(cmdData)) {\n                    for (const item of cmdData) {\n                        if (cmd === \"ADD\") {\n                            recordList.add(item);\n                        } else if (cmd === \"ADD.noinv\") {\n                            recordList._.addNoinv(recordList, item);\n                        } else if (cmd === \"DELETE.noinv\") {\n                            recordList._.deleteNoinv(recordList, item);\n                        } else {\n                            recordList.delete(item);\n                        }\n                    }\n                } else {\n                    if (cmd === \"ADD\") {\n                        recordList.add(cmdData);\n                    } else if (cmd === \"ADD.noinv\") {\n                        recordList._.addNoinv(recordList, cmdData);\n                    } else if (cmd === \"DELETE.noinv\") {\n                        recordList._.deleteNoinv(recordList, cmdData);\n                    } else {\n                        recordList.delete(cmdData);\n                    }\n                }\n            }\n        } else if ([null, false, undefined].includes(value)) {\n            recordList.clear();\n        } else if (!Array.isArray(value)) {\n            recordList._.assign(recordList, [value]);\n        } else {\n            recordList._.assign(recordList, value);\n        }\n    }\n    /**\n     * @param {RecordList} recordList\n     * @param {any} value\n     * @returns {boolean} whether the value has changed\n     */\n    updateRelationOne(recordList, value) {\n        if (isCommand(value)) {\n            const [cmd, cmdData] = value.at(-1);\n            if (cmd === \"ADD\") {\n                recordList.add(cmdData);\n            } else if (cmd === \"ADD.noinv\") {\n                recordList._.addNoinv(recordList, cmdData);\n            } else if (cmd === \"DELETE.noinv\") {\n                recordList._.deleteNoinv(recordList, cmdData);\n            } else {\n                recordList.delete(cmdData);\n            }\n        } else if ([null, false, undefined].includes(value)) {\n            recordList.clear();\n        } else {\n            recordList.add(value);\n        }\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\n\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useFileViewer } from \"@web/core/file_viewer/file_viewer_hook\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { url } from \"@web/core/utils/urls\";\n\nclass ImageActions extends Component {\n    static components = { Dropdown, DropdownItem };\n    static props = [\"actions\", \"imagesHeight\"];\n    static template = \"mail.ImageActions\";\n\n    setup() {\n        super.setup();\n        this.actionsMenuState = useDropdownState();\n        this.isMobileOS = isMobileOS;\n    }\n}\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Attachment[]} attachments\n * @property {function} unlinkAttachment\n * @property {number} imagesHeight\n * @property {ReturnType<import('@mail/core/common/message_search_hook').useMessageSearch>} [messageSearch]\n * @extends {Component<Props, Env>}\n */\nexport class AttachmentList extends Component {\n    static components = { ImageActions };\n    static props = [\"attachments\", \"unlinkAttachment\", \"imagesHeight\", \"messageSearch?\"];\n    static template = \"mail.AttachmentList\";\n\n    setup() {\n        super.setup();\n        this.ui = useState(useService(\"ui\"));\n        // Arbitrary high value, this is effectively a max-width.\n        this.imagesWidth = 1920;\n        this.dialog = useService(\"dialog\");\n        this.fileViewer = useFileViewer();\n        this.actionsMenuState = useDropdownState();\n        this.isMobileOS = isMobileOS;\n    }\n\n    /**\n     * @param {import(\"models\").Attachment} attachment\n     */\n    getImageUrl(attachment) {\n        if (attachment.uploading && attachment.tmpUrl) {\n            return attachment.tmpUrl;\n        }\n        return url(attachment.urlRoute, {\n            ...attachment.urlQueryParams,\n            width: this.imagesWidth * 2,\n            height: this.props.imagesHeight * 2,\n        });\n    }\n\n    get images() {\n        return this.props.attachments.filter((a) => a.isImage);\n    }\n\n    get cards() {\n        return this.props.attachments.filter((a) => !a.isImage);\n    }\n\n    /**\n     * @param {import(\"models\").Attachment} attachment\n     */\n    canDownload(attachment) {\n        return !attachment.uploading && !this.env.inComposer;\n    }\n\n    /**\n     * @param {import(\"models\").Attachment} attachment\n     */\n    onClickDownload(attachment) {\n        const downloadLink = document.createElement(\"a\");\n        downloadLink.setAttribute(\"href\", attachment.downloadUrl);\n        // Adding 'download' attribute into a link prevents open a new\n        // tab or change the current location of the window. This avoids\n        // interrupting the activity in the page such as rtc call.\n        downloadLink.setAttribute(\"download\", \"\");\n        downloadLink.click();\n    }\n\n    /**\n     * @param {import(\"models\").Attachment} attachment\n     */\n    onClickUnlink(attachment) {\n        if (this.env.inComposer) {\n            return this.props.unlinkAttachment(attachment);\n        }\n        this.dialog.add(ConfirmationDialog, {\n            body: _t('Do you really want to delete \"%s\"?', attachment.filename),\n            cancel: () => {},\n            confirm: () => this.onConfirmUnlink(attachment),\n        });\n    }\n\n    /**\n     * @param {import(\"models\").Attachment} attachment\n     */\n    onConfirmUnlink(attachment) {\n        this.props.unlinkAttachment(attachment);\n    }\n\n    onImageLoaded() {\n        this.env.onImageLoaded?.();\n    }\n\n    get isInChatWindowAndIsAlignedRight() {\n        return this.env.inChatWindow && this.env.alignedRight;\n    }\n\n    get isInChatWindowAndIsAlignedLeft() {\n        return this.env.inChatWindow && !this.env.alignedRight;\n    }\n\n    getActions(attachment) {\n        const res = [];\n        if (this.showDelete) {\n            res.push({\n                label: \"Remove\",\n                icon: \"fa fa-trash\",\n                onSelect: () => this.onClickUnlink(attachment),\n            });\n        }\n        if (this.canDownload(attachment)) {\n            res.push({\n                label: \"Download\",\n                icon: \"fa fa-download\",\n                onSelect: () => this.onClickDownload(attachment),\n            });\n        }\n        return res;\n    }\n\n    get showDelete() {\n        // in the composer they should all be implicitly deletable\n        if (this.env.inComposer) {\n            return true;\n        }\n        if (!this.attachment.isDeletable) {\n            return false;\n        }\n        // in messages users are expected to delete the message instead of just the attachment\n        return (\n            !this.env.message ||\n            this.env.message.hasTextContent ||\n            (this.env.message && this.props.attachments.length > 1)\n        );\n    }\n}\n", "import { Record } from \"@mail/core/common/record\";\nimport { assignDefined } from \"@mail/utils/common/misc\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { FileModelMixin } from \"@web/core/file_viewer/file_model\";\n\nexport class Attachment extends FileModelMixin(Record) {\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").Attachment>} */\n    static records = {};\n    /** @returns {import(\"models\").Attachment} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Attachment|import(\"models\").Attachment[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n    static new() {\n        /** @type {import(\"models\").Attachment} */\n        const attachment = super.new(...arguments);\n        Record.onChange(attachment, [\"extension\", \"filename\"], () => {\n            if (!attachment.extension && attachment.filename) {\n                attachment.extension = attachment.filename.split(\".\").pop();\n            }\n        });\n        return attachment;\n    }\n\n    thread = Record.one(\"Thread\", { inverse: \"attachments\" });\n    res_name;\n    message = Record.one(\"Message\", { inverse: \"attachment_ids\" });\n    /** @type {luxon.DateTime} */\n    create_date = Record.attr(undefined, { type: \"datetime\" });\n\n    get isDeletable() {\n        return true;\n    }\n\n    get monthYear() {\n        if (!this.create_date) {\n            return undefined;\n        }\n        return `${this.create_date.monthLong}, ${this.create_date.year}`;\n    }\n\n    get uploading() {\n        return this.id < 0;\n    }\n\n    /** Remove the given attachment globally. */\n    delete() {\n        if (this.tmpUrl) {\n            URL.revokeObjectURL(this.tmpUrl);\n        }\n        super.delete();\n    }\n\n    /**\n     * Delete the given attachment on the server as well as removing it\n     * globally.\n     */\n    async remove() {\n        if (this.id > 0) {\n            const rpcParams = assignDefined(\n                { attachment_id: this.id },\n                { access_token: this.access_token }\n            );\n            const thread = this.thread || this.message?.thread;\n            if (thread) {\n                Object.assign(rpcParams, thread.rpcParams);\n            }\n            await rpc(\"/mail/attachment/delete\", rpcParams);\n        }\n        this.delete();\n    }\n}\n\nAttachment.register();\n", "import { EventBus } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\nexport class AttachmentUploadService {\n    constructor(env, services) {\n        this.setup(env, services);\n    }\n\n    setup(env, services) {\n        this.env = env;\n        this.fileUploadService = services[\"file_upload\"];\n        /** @type {import(\"@mail/core/common/store_service\").Store} */\n        this.store = services[\"mail.store\"];\n        this.notificationService = services[\"notification\"];\n\n        this.nextId = -1;\n        this.abortByAttachmentId = new Map();\n        this.deferredByAttachmentId = new Map();\n        this.uploadingAttachmentIds = new Set();\n        this._fileUploadBus = new EventBus();\n        /** @type {Map<number, {composer: import(\"models\").Composer, thread: import(\"models\").Thread}>} */\n        this.targetsByTmpId = new Map();\n        this.fileUploadService.bus.addEventListener(\n            \"FILE_UPLOAD_ADDED\",\n            ({ detail: { upload } }) => {\n                const tmpId = parseInt(upload.data.get(\"temporary_id\"));\n                if (!this.uploadingAttachmentIds.has(tmpId)) {\n                    return;\n                }\n                const { thread, composer } = this.targetsByTmpId.get(tmpId);\n                const tmpUrl = upload.data.get(\"tmp_url\");\n                this.abortByAttachmentId.set(tmpId, upload.xhr.abort.bind(upload.xhr));\n                const attachment = this.store.Attachment.insert(\n                    this._makeAttachmentData(upload, tmpId, composer ? undefined : thread, tmpUrl)\n                );\n                composer?.attachments.push(attachment);\n            }\n        );\n        this.fileUploadService.bus.addEventListener(\n            \"FILE_UPLOAD_LOADED\",\n            ({ detail: { upload } }) => {\n                const tmpId = parseInt(upload.data.get(\"temporary_id\"));\n                if (!this.uploadingAttachmentIds.has(tmpId)) {\n                    return;\n                }\n                const def = this.deferredByAttachmentId.get(tmpId);\n                if (upload.xhr.status === 413) {\n                    this.notificationService.add(_t(\"File too large\"), { type: \"danger\" });\n                    def.resolve();\n                    this._cleanupUploading(tmpId);\n                    return;\n                }\n                if (upload.xhr.status !== 200) {\n                    this.notificationService.add(_t(\"Server error\"), { type: \"danger\" });\n                    def.resolve();\n                    this._cleanupUploading(tmpId);\n                    return;\n                }\n                const response = JSON.parse(upload.xhr.response);\n                if (response.error) {\n                    this.notificationService.add(response.error, { type: \"danger\" });\n                    def.resolve();\n                    this._cleanupUploading(tmpId);\n                    return;\n                }\n                const { thread, composer } = this.targetsByTmpId.get(tmpId);\n                // FIXME: this should be only response. HOOT tests returns wrong data {result, error}\n                const attachmentData = response?.result ?? response;\n                this._processLoaded(thread, composer, attachmentData, tmpId, def);\n            }\n        );\n        this.fileUploadService.bus.addEventListener(\n            \"FILE_UPLOAD_ERROR\",\n            ({ detail: { upload } }) => {\n                const tmpId = parseInt(upload.data.get(\"temporary_id\"));\n                if (!this.uploadingAttachmentIds.has(tmpId)) {\n                    return;\n                }\n                this.deferredByAttachmentId.get(tmpId).resolve();\n                this._cleanupUploading(tmpId);\n            }\n        );\n    }\n\n    _processLoaded(thread, composer, { data }, tmpId, def) {\n        const { Attachment } = this.store.insert(data);\n        const [attachment] = Attachment;\n        if (composer) {\n            const index = composer.attachments.findIndex(({ id }) => id === tmpId);\n            if (index >= 0) {\n                composer.attachments[index] = attachment;\n            } else {\n                composer.attachments.push(attachment);\n            }\n        }\n        def.resolve(attachment);\n        this._fileUploadBus.trigger(\"UPLOAD\", thread);\n        this._cleanupUploading(tmpId);\n    }\n\n    _cleanupUploading(tmpId) {\n        this.abortByAttachmentId.delete(tmpId);\n        this.deferredByAttachmentId.delete(tmpId);\n        this.uploadingAttachmentIds.delete(tmpId);\n        this.targetsByTmpId.delete(tmpId);\n        this.store.Attachment.get(tmpId).remove();\n    }\n\n    getUploadURL(thread) {\n        return \"/mail/attachment/upload\";\n    }\n\n    async unlink(attachment) {\n        if (this.uploadingAttachmentIds.has(attachment.id)) {\n            const deferred = this.deferredByAttachmentId.get(attachment.id);\n            const abort = this.abortByAttachmentId.get(attachment.id);\n            this._cleanupUploading(attachment.id);\n            deferred.resolve();\n            abort();\n            return;\n        }\n        await attachment.remove();\n    }\n\n    async upload(thread, composer, file, options) {\n        const tmpId = this.nextId--;\n        const tmpURL = URL.createObjectURL(file);\n        return this._upload(thread, composer, file, options, tmpId, tmpURL);\n    }\n\n    async _upload(thread, composer, file, options, tmpId, tmpURL) {\n        this.targetsByTmpId.set(tmpId, { composer, thread });\n        this.uploadingAttachmentIds.add(tmpId);\n        await this.fileUploadService\n            .upload(this.getUploadURL(thread), [file], {\n                buildFormData: (formData) => {\n                    this._buildFormData(formData, tmpURL, thread, composer, tmpId, options);\n                },\n            })\n            .catch((e) => {\n                if (e.name !== \"AbortError\") {\n                    throw e;\n                }\n            });\n        const uploadDoneDeferred = new Deferred();\n        this.deferredByAttachmentId.set(tmpId, uploadDoneDeferred);\n        return uploadDoneDeferred;\n    }\n\n    /**\n     * @param {import(\"models\").Thread} thread\n     * @param {() => void} onFileUploaded\n     */\n    onFileUploaded(thread, onFileUploaded) {\n        this._fileUploadBus.addEventListener(\"UPLOAD\", ({ detail }) => {\n            if (thread.eq(detail)) {\n                onFileUploaded();\n            }\n        });\n    }\n\n    _buildFormData(formData, tmpURL, thread, composer, tmpId, options) {\n        formData.append(\"thread_id\", thread.id);\n        formData.append(\"tmp_url\", tmpURL);\n        formData.append(\"thread_model\", thread.model);\n        formData.append(\"is_pending\", Boolean(composer));\n        formData.append(\"temporary_id\", tmpId);\n        if (options?.activity) {\n            formData.append(\"activity_id\", options.activity.id);\n        }\n        return formData;\n    }\n\n    _makeAttachmentData(upload, tmpId, thread, tmpUrl) {\n        const attachmentData = {\n            filename: upload.title,\n            id: tmpId,\n            mimetype: upload.type,\n            name: upload.title,\n            thread,\n            extension: upload.title.split(\".\").pop(),\n            uploading: true,\n            tmpUrl,\n        };\n        return attachmentData;\n    }\n}\n\nexport const attachmentUploadService = {\n    dependencies: [\"file_upload\", \"mail.store\", \"notification\"],\n    start(env, services) {\n        return new AttachmentUploadService(env, services);\n    },\n};\n\nregistry.category(\"services\").add(\"mail.attachment_upload\", attachmentUploadService);\n", "import { useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\nfunction dataUrlToBlob(data, type) {\n    const binData = window.atob(data);\n    const uiArr = new Uint8Array(binData.length);\n    uiArr.forEach((_, index) => (uiArr[index] = binData.charCodeAt(index)));\n    return new Blob([uiArr], { type });\n}\n\nexport class AttachmentUploader {\n    constructor(thread, { composer } = {}) {\n        this.attachmentUploadService = useService(\"mail.attachment_upload\");\n        Object.assign(this, { thread, composer });\n    }\n\n    uploadData({ data, name, type }, options) {\n        const file = new File([dataUrlToBlob(data, type)], name, { type });\n        return this.uploadFile(file, options);\n    }\n\n    async uploadFile(file, options) {\n        return this.attachmentUploadService.upload(this.thread, this.composer, file, options);\n    }\n\n    async unlink(attachment) {\n        await this.attachmentUploadService.unlink(attachment);\n    }\n}\n\n/**\n * @param {import(\"models\").Thread} thread\n * @param {Object} [param1={}]\n * @param {import(\"models\").Composer} [param1.composer]\n * @param {function} [param1.onFileUploaded]\n */\nexport function useAttachmentUploader(thread, { composer, onFileUploaded } = {}) {\n    return useState(new AttachmentUploader(...arguments));\n}\n", "import {\n    Component,\n    onWillUpdateProps,\n    onPatched,\n    onWillUnmount,\n    onMounted,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { hidePDFJSButtons } from \"@web/libs/pdfjs\";\n\n/**\n * @typedef {Object} Props\n * @property {number} threadId\n * @property {string} threadModel\n * @extends {Component<Props, Env>}\n */\nexport class AttachmentView extends Component {\n    static template = \"mail.AttachmentView\";\n    static components = {};\n    static props = [\"threadId\", \"threadModel\"];\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.uiService = useService(\"ui\");\n        this.mailPopoutService = useService(\"mail.popout\");\n        this.iframeViewerPdfRef = useRef(\"iframeViewerPdf\");\n        this.state = useState({\n            /** @type {import(\"models\").Thread|undefined} */\n            thread: undefined,\n        });\n        useEffect(() => {\n            if (this.iframeViewerPdfRef.el) {\n                hidePDFJSButtons(this.iframeViewerPdfRef.el);\n            }\n        });\n        this.updateFromProps(this.props);\n        onWillUpdateProps((props) => this.updateFromProps(props));\n\n        onMounted(this.updatePopout);\n        onPatched(this.updatePopout);\n        onWillUnmount(this.resetPopout);\n    }\n\n    onClickNext() {\n        const index = this.state.thread.attachmentsInWebClientView.findIndex((attachment) =>\n            attachment.eq(this.state.thread.mainAttachment)\n        );\n        this.state.thread.setMainAttachmentFromIndex(\n            index === this.state.thread.attachmentsInWebClientView.length - 1 ? 0 : index + 1\n        );\n    }\n\n    onClickPrevious() {\n        const index = this.state.thread.attachmentsInWebClientView.findIndex((attachment) =>\n            attachment.eq(this.state.thread.mainAttachment)\n        );\n        this.state.thread.setMainAttachmentFromIndex(\n            index === 0 ? this.state.thread.attachmentsInWebClientView.length - 1 : index - 1\n        );\n    }\n\n    updateFromProps(props) {\n        this.state.thread = this.store.Thread.insert({\n            id: props.threadId,\n            model: props.threadModel,\n        });\n    }\n\n    popoutAttachment() {\n        this.mailPopoutService.addHooks(\n            () => {\n                // before popout hook\n                this.hide();\n                this.uiService.bus.trigger(\"resize\");\n            },\n            () => {\n                // after popout hook\n                this.show();\n                this.uiService.bus.trigger(\"resize\");\n            }\n        );\n        this.mailPopoutService.popout(PopoutAttachmentView, this.props);\n    }\n\n    get attachmentViewParentElementClassList() {\n        const attachmentViewEl = document.querySelector(\".o-mail-Attachment\");\n        let parentElementClassList;\n        if ((parentElementClassList = attachmentViewEl?.parentElement?.classList)) {\n            return parentElementClassList;\n        }\n        return null;\n    }\n\n    show() {\n        const parentElementClassList = this.attachmentViewParentElementClassList;\n        const hiddenClass = \"d-none\";\n        if (parentElementClassList?.contains(hiddenClass)) {\n            parentElementClassList.remove(hiddenClass);\n        }\n    }\n\n    hide() {\n        const parentElementClassList = this.attachmentViewParentElementClassList;\n        const hiddenClass = \"d-none\";\n        if (!parentElementClassList?.contains(hiddenClass)) {\n            parentElementClassList.add(hiddenClass);\n        }\n    }\n\n    updatePopout() {\n        if (this.mailPopoutService.externalWindow) {\n            this.mailPopoutService.popout(PopoutAttachmentView, this.props);\n            this.hide();\n        }\n    }\n\n    resetPopout() {\n        this.mailPopoutService.reset();\n    }\n\n    get displayName() {\n        return this.state.thread.mainAttachment.filename;\n    }\n}\n\n/*\n * AttachmentView inside popout window.\n * Popout features disabled as this only makes sense in the non-popout AttachmentView.\n */\nclass PopoutAttachmentView extends AttachmentView {\n    static template = \"mail.PopoutAttachmentView\";\n    updatePopout() {}\n    resetPopout() {}\n}\n", "import { Component, useRef, useState, onWillUpdateProps, onMounted } from \"@odoo/owl\";\n\nimport { useAutoresize } from \"@web/core/utils/autoresize\";\n\nexport class AutoresizeInput extends Component {\n    static template = \"mail.AutoresizeInput\";\n    static props = {\n        autofocus: { type: Boolean, optional: true },\n        className: { type: String, optional: true },\n        enabled: { optional: true },\n        onValidate: { type: Function, optional: true },\n        placeholder: { type: String, optional: true },\n        value: { type: String, optional: true },\n    };\n    static defaultProps = {\n        autofocus: false,\n        className: \"\",\n        enabled: true,\n        onValidate: () => {},\n        placeholder: \"\",\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            value: this.props.value,\n        });\n        this.inputRef = useRef(\"input\");\n        onWillUpdateProps((nextProps) => {\n            if (this.props.value !== nextProps.value) {\n                this.state.value = nextProps.value;\n            }\n        });\n        useAutoresize(this.inputRef);\n        onMounted(() => {\n            if (this.props.autofocus) {\n                this.inputRef.el.focus();\n                this.inputRef.el.setSelectionRange(-1, -1);\n            }\n        });\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onKeydownInput(ev) {\n        switch (ev.key) {\n            case \"Enter\":\n                this.inputRef.el.blur();\n                break;\n            case \"Escape\":\n                this.state.value = this.props.value;\n                this.inputRef.el.blur();\n                break;\n        }\n    }\n}\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class CannedResponse extends Record {\n    static _name = \"mail.canned.response\";\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").CannedResponse>} */\n    static records = {};\n    /** @returns {import(\"models\").CannedResponse} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").CannedResponse|import(\"models\").CannedResponse[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    source;\n    /** @type {string} */\n    substitution;\n}\n\nCannedResponse.register();\n", "import { Store } from \"@mail/core/common/store_service\";\nimport { Record } from \"@mail/core/common/record\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { deserializeDateTime } from \"@web/core/l10n/dates\";\nimport { user } from \"@web/core/user\";\n\nconst { DateTime } = luxon;\n\nexport class ChannelMember extends Record {\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").ChannelMember>} */\n    static records = {};\n    /** @returns {import(\"models\").ChannelMember} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").ChannelMember|import(\"models\").ChannelMember[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    /** @type {string} */\n    create_date;\n    /** @type {number} */\n    id;\n    /** @type {luxon.DateTime} */\n    last_interest_dt = Record.attr(undefined, { type: \"datetime\" });\n    /** @type {luxon.DateTime} */\n    last_seen_dt = Record.attr(undefined, { type: \"datetime\" });\n    persona = Record.one(\"Persona\", { inverse: \"channelMembers\" });\n    thread = Record.one(\"Thread\", { inverse: \"channelMembers\" });\n    threadAsSelf = Record.one(\"Thread\", {\n        compute() {\n            if (this.store.self?.eq(this.persona)) {\n                return this.thread;\n            }\n        },\n    });\n    fetched_message_id = Record.one(\"Message\");\n    seen_message_id = Record.one(\"Message\");\n    syncUnread = true;\n    _syncUnread = Record.attr(false, {\n        compute() {\n            if (!this.syncUnread || !this.eq(this.thread?.selfMember)) {\n                return false;\n            }\n            return (\n                this.localNewMessageSeparator !== this.new_message_separator ||\n                this.localMessageUnreadCounter !== this.message_unread_counter\n            );\n        },\n        onUpdate() {\n            if (this._syncUnread) {\n                this.localNewMessageSeparator = this.new_message_separator;\n                this.localMessageUnreadCounter = this.message_unread_counter;\n            }\n        },\n    });\n    unreadSynced = Record.attr(true, {\n        compute() {\n            return this.localNewMessageSeparator === this.new_message_separator;\n        },\n        onUpdate() {\n            if (this.unreadSynced) {\n                this.hideUnreadBanner = false;\n            }\n        },\n    });\n    hideUnreadBanner = false;\n    localMessageUnreadCounter = 0;\n    localNewMessageSeparator = null;\n    message_unread_counter = 0;\n    message_unread_counter_bus_id = 0;\n    new_message_separator = null;\n    threadAsTyping = Record.one(\"Thread\", {\n        compute() {\n            return this.isTyping ? this.thread : undefined;\n        },\n        eager: true,\n        onAdd() {\n            browser.clearTimeout(this.typingTimeoutId);\n            this.typingTimeoutId = browser.setTimeout(\n                () => (this.isTyping = false),\n                Store.OTHER_LONG_TYPING\n            );\n        },\n        onDelete() {\n            browser.clearTimeout(this.typingTimeoutId);\n        },\n    });\n    /** @type {number} */\n    typingTimeoutId;\n\n    get name() {\n        return this.persona.name;\n    }\n\n    /**\n     * @returns {string}\n     */\n    getLangName() {\n        return this.persona.lang_name;\n    }\n\n    get memberSince() {\n        return this.create_date ? deserializeDateTime(this.create_date) : undefined;\n    }\n\n    /**\n     * @param {import(\"models\").Message} message\n     */\n    hasSeen(message) {\n        return this.persona.eq(message.author) || this.seen_message_id?.id >= message.id;\n    }\n    get lastSeenDt() {\n        return this.last_seen_dt\n            ? this.last_seen_dt.toLocaleString(DateTime.TIME_24_SIMPLE, {\n                  locale: user.lang,\n              })\n            : undefined;\n    }\n\n    get totalUnreadMessageCounter() {\n        let counter = this.message_unread_counter;\n        if (!this.unreadSynced) {\n            counter += this.localMessageUnreadCounter;\n        }\n        return counter;\n    }\n}\n\nChannelMember.register();\n", "import { ImStatus } from \"@mail/core/common/im_status\";\n\nimport { Component, useEffect, useRef, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useHover, useMovable } from \"@mail/utils/common/hooks\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { CountryFlag } from \"@mail/core/common/country_flag\";\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class ChatBubble extends Component {\n    static components = { CountryFlag, ImStatus, Dropdown };\n    static props = [\"chatWindow\"];\n    static template = \"mail.ChatBubble\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.wasHover = false;\n        this.hover = useHover([\"root\", \"preview*\"], {\n            onHover: () => (this.preview.isOpen = true),\n            onHovering: [100, () => (this.state.showClose = true)],\n            onAway: () => {\n                this.state.showClose = false;\n                this.preview.isOpen = false;\n            },\n        });\n        this.preview = useDropdownState();\n        this.rootRef = useRef(\"root\");\n        this.state = useState({ bouncing: false, showClose: true });\n        useEffect(\n            () => {\n                this.state.bouncing = this.thread.importantCounter ? true : this.state.bouncing;\n            },\n            () => [this.thread.importantCounter]\n        );\n        if (this.env.embedLivechat) {\n            this.position = useState({ left: \"auto\", top: \"auto\" });\n            useMovable({\n                cursor: \"grabbing\",\n                ref: this.rootRef,\n                elements: \".o-mail-ChatBubble\",\n                onDrop: ({ top, left }) =>\n                    Object.assign(this.position, { left: `${left}px`, top: `${top}px` }),\n            });\n        }\n    }\n\n    /** @returns {import(\"models\").Thread} */\n    get thread() {\n        return this.props.chatWindow.thread;\n    }\n\n    get previewContent() {\n        const lastMessage = this.thread?.newestPersistentNotEmptyOfAllMessage;\n        if (!lastMessage) {\n            return false;\n        }\n        return lastMessage.inlineBody;\n    }\n}\n", "import { ChatWindow } from \"@mail/core/common/chat_window\";\nimport { useHover, useMovable } from \"@mail/utils/common/hooks\";\nimport { Component, useEffect, useExternalListener, useRef, useState } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ChatBubble } from \"./chat_bubble\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ChatHub extends Component {\n    static components = { ChatBubble, ChatWindow, Dropdown };\n    static props = [];\n    static template = \"mail.ChatHub\";\n\n    get chatHub() {\n        return this.store.chatHub;\n    }\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.ui = useState(useService(\"ui\"));\n        this.bubblesHover = useHover(\"bubbles\");\n        this.moreHover = useHover([\"more-button\", \"more-menu*\"], {\n            onHover: () => (this.more.isOpen = true),\n            onAway: () => (this.more.isOpen = false),\n        });\n        this.options = useDropdownState();\n        this.more = useDropdownState();\n        this.compactRef = useRef(\"compact\");\n        this.compactPosition = useState({ left: \"auto\", top: \"auto\" });\n        this.onResize();\n        useExternalListener(browser, \"resize\", this.onResize);\n        useEffect(() => {\n            if (this.chatHub.folded.length && this.store.channels?.status === \"not_fetched\") {\n                this.store.channels.fetch();\n            }\n        });\n        useMovable({\n            cursor: \"grabbing\",\n            ref: this.compactRef,\n            elements: \".o-mail-ChatHub-compact\",\n            onDrop: ({ top, left }) =>\n                Object.assign(this.compactPosition, { left: `${left}px`, top: `${top}px` }),\n        });\n    }\n\n    onResize() {\n        this.chatHub.onRecompute();\n    }\n\n    get chatSizeTransitionText() {\n        return this.chatHub.isBig ? _t(\"Make chats smaller\") : _t(\"Make chats bigger\");\n    }\n\n    get compactCounter() {\n        let counter = 0;\n        const cws = this.chatHub.opened.concat(this.chatHub.folded);\n        for (const chatWindow of cws) {\n            counter += chatWindow.thread.importantCounter > 0 ? 1 : 0;\n        }\n        return counter;\n    }\n\n    toggleChatSize() {\n        this.chatHub.isBig = !this.chatHub.isBig;\n    }\n\n    get hiddenCounter() {\n        let counter = 0;\n        for (const chatWindow of this.chatHub.folded.slice(this.chatHub.maxFolded)) {\n            counter += chatWindow.thread.importantCounter > 0 ? 1 : 0;\n        }\n        return counter;\n    }\n\n    get isShown() {\n        return !this.ui.isSmall;\n    }\n\n    expand() {\n        this.chatHub.compact = false;\n        Object.assign(this.compactPosition, { left: \"auto\", top: \"auto\" });\n        this.more.isOpen = this.chatHub.folded.length > this.chatHub.maxFolded;\n    }\n}\n\nregistry.category(\"main_components\").add(\"mail.ChatHub\", { Component: ChatHub });\n", "import { browser } from \"@web/core/browser/browser\";\nimport { Record } from \"./record\";\n\nexport class ChatHub extends Record {\n    BUBBLE = 56; // same value as $o-mail-ChatHub-bubblesWidth\n    BUBBLE_START = 15; // same value as $o-mail-ChatHub-bubblesStart\n    BUBBLE_LIMIT = 7;\n    BUBBLE_OUTER = 10; // same value as $o-mail-ChatHub-bubblesMargin\n    WINDOW_GAP = 10; // for a single end, multiply by 2 for left and right together.\n    WINDOW_INBETWEEN = 5;\n    WINDOW = 360; // same value as $o-mail-ChatWindow-width\n    WINDOW_LARGE = 510; // same value as $o-mail-ChatWindow-widthLarge\n\n    /** @returns {import(\"models\").ChatHub} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").ChatHub|import(\"models\").ChatHub[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n    isBig = Record.attr(false, {\n        compute() {\n            return browser.localStorage.getItem(\"mail.user_setting.chat_window_big\") === \"true\";\n        },\n        onUpdate() {\n            /** @this {import(\"models\").ChatHub} */\n            if (this.isBig) {\n                browser.localStorage.setItem(\n                    \"mail.user_setting.chat_window_big\",\n                    this.isBig.toString()\n                );\n            } else {\n                browser.localStorage.removeItem(\"mail.user_setting.chat_window_big\");\n            }\n        },\n    });\n    compact = false;\n    /** From left to right. Right-most will actually be folded */\n    opened = Record.many(\"ChatWindow\", {\n        inverse: \"hubAsOpened\",\n        /** @this {import(\"models\").ChatHub} */\n        onAdd(r) {\n            this.onRecompute();\n        },\n        /** @this {import(\"models\").ChatHub} */\n        onDelete() {\n            this.onRecompute();\n        },\n    });\n    /** From top to bottom. Bottom-most will actually be hidden */\n    folded = Record.many(\"ChatWindow\", {\n        inverse: \"hubAsFolded\",\n        /** @this {import(\"models\").ChatHub} */\n        onAdd(r) {\n            this.onRecompute();\n        },\n        /** @this {import(\"models\").ChatHub} */\n        onDelete() {\n            this.onRecompute();\n        },\n    });\n\n    closeAll() {\n        [...this.opened, ...this.folded].forEach((cw) => cw.close());\n    }\n\n    onRecompute() {\n        while (this.opened.length > this.maxOpened) {\n            const cw = this.opened.pop();\n            this.folded.unshift(cw);\n        }\n    }\n\n    get maxOpened() {\n        const chatBubblesWidth = this.BUBBLE_START + this.BUBBLE + this.BUBBLE_OUTER * 2;\n        const startGap = this.store.env.services.ui.isSmall ? 0 : this.WINDOW_GAP;\n        const endGap = this.store.env.services.ui.isSmall ? 0 : this.WINDOW_GAP;\n        const available = browser.innerWidth - startGap - endGap - chatBubblesWidth;\n        const maxAmountWithoutHidden = Math.max(\n            1,\n            Math.floor(\n                available / ((this.isBig ? this.WINDOW_LARGE : this.WINDOW) + this.WINDOW_INBETWEEN)\n            )\n        );\n        return maxAmountWithoutHidden;\n    }\n\n    get maxFolded() {\n        const chatBubbleSpace = this.BUBBLE_START + this.BUBBLE + this.BUBBLE_OUTER * 2;\n        return Math.min(this.BUBBLE_LIMIT, Math.floor(browser.innerHeight / chatBubbleSpace));\n    }\n}\n\nChatHub.register();\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { ImStatus } from \"@mail/core/common/im_status\";\nimport { Thread } from \"@mail/core/common/thread\";\nimport { AutoresizeInput } from \"@mail/core/common/autoresize_input\";\nimport { CountryFlag } from \"@mail/core/common/country_flag\";\nimport { useThreadActions } from \"@mail/core/common/thread_actions\";\nimport { ThreadIcon } from \"@mail/core/common/thread_icon\";\nimport {\n    useHover,\n    useMessageEdition,\n    useMessageHighlight,\n    useMessageToReplyTo,\n} from \"@mail/utils/common/hooks\";\nimport { isEventHandled } from \"@web/core/utils/misc\";\n\nimport { Component, toRaw, useChildSubEnv, useRef, useState } from \"@odoo/owl\";\n\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Typing } from \"@mail/discuss/typing/common/typing\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").ChatWindow} chatWindow\n * @property {boolean} [right]\n * @extends {Component<Props, Env>}\n */\nexport class ChatWindow extends Component {\n    static components = {\n        CountryFlag,\n        Dropdown,\n        DropdownItem,\n        Thread,\n        Composer,\n        ThreadIcon,\n        ImStatus,\n        AutoresizeInput,\n        Typing,\n    };\n    static props = [\"chatWindow\", \"right?\"];\n    static template = \"mail.ChatWindow\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.messageEdition = useMessageEdition();\n        this.messageHighlight = useMessageHighlight();\n        this.messageToReplyTo = useMessageToReplyTo();\n        this.state = useState({\n            actionsDisabled: false,\n            actionsMenuOpened: false,\n            jumpThreadPresent: 0,\n            editingGuestName: false,\n            editingName: false,\n        });\n        this.ui = useState(useService(\"ui\"));\n        this.contentRef = useRef(\"content\");\n        this.threadActions = useThreadActions();\n        this.actionsMenuButtonHover = useHover(\"actionsMenuButton\");\n        this.parentChannelHover = useHover(\"parentChannel\");\n\n        useChildSubEnv({\n            closeActionPanel: () => this.threadActions.activeAction?.close(),\n            inChatWindow: true,\n            messageHighlight: this.messageHighlight,\n        });\n    }\n\n    get composerType() {\n        if (this.thread && this.thread.model !== \"discuss.channel\") {\n            return \"note\";\n        }\n        return undefined;\n    }\n\n    get thread() {\n        return this.props.chatWindow.thread;\n    }\n\n    get style() {\n        const maxHeight = !this.ui.isSmall ? \"max-height: 95vh;\" : \"\";\n        const textDirection = localization.direction;\n        const offsetFrom = textDirection === \"rtl\" ? \"left\" : \"right\";\n        const visibleOffset = this.ui.isSmall ? 0 : this.props.right;\n        const oppositeFrom = offsetFrom === \"right\" ? \"left\" : \"right\";\n        return `${offsetFrom}: ${visibleOffset}px; ${oppositeFrom}: auto; ${maxHeight}`;\n    }\n\n    onKeydown(ev) {\n        const chatWindow = toRaw(this.props.chatWindow);\n        if (ev.target.closest(\".o-dropdown\") || ev.target.closest(\".o-dropdown--menu\")) {\n            return;\n        }\n        ev.stopPropagation(); // not letting home menu steal my CTRL-C\n        switch (ev.key) {\n            case \"Escape\":\n                if (\n                    isEventHandled(ev, \"NavigableList.close\") ||\n                    isEventHandled(ev, \"Composer.discard\")\n                ) {\n                    return;\n                }\n                if (this.state.editingName) {\n                    this.state.editingName = false;\n                    return;\n                }\n                this.close({ escape: true });\n                break;\n            case \"Tab\": {\n                const index = this.store.chatHub.opened.findIndex((cw) => cw.eq(chatWindow));\n                if (index === this.store.chatHub.opened.length - 1) {\n                    this.store.chatHub.opened[0].focus();\n                } else {\n                    this.store.chatHub.opened[index + 1].focus();\n                }\n                break;\n            }\n        }\n    }\n\n    onClickHeader() {\n        if (\n            this.ui.isSmall ||\n            this.state.editingName ||\n            !this.thread ||\n            this.state.actionsDisabled\n        ) {\n            return;\n        }\n        this.toggleFold();\n    }\n\n    toggleFold() {\n        const chatWindow = toRaw(this.props.chatWindow);\n        if (this.ui.isSmall || this.state.actionsMenuOpened) {\n            return;\n        }\n        chatWindow.fold();\n    }\n\n    async close(options) {\n        const chatWindow = toRaw(this.props.chatWindow);\n        await chatWindow.close(options);\n    }\n\n    get actionsMenuTitleText() {\n        return _t(\"Open Actions Menu\");\n    }\n\n    async renameThread(name) {\n        const thread = toRaw(this.thread);\n        await thread.rename(name);\n        this.state.editingName = false;\n    }\n\n    async renameGuest(name) {\n        const newName = name.trim();\n        if (this.store.self.name !== newName) {\n            await this.store.self.updateGuestName(newName);\n        }\n        this.state.editingGuestName = false;\n    }\n\n    async onActionsMenuStateChanged(isOpen) {\n        // await new Promise(setTimeout); // wait for bubbling header\n        this.state.actionsMenuOpened = isOpen;\n    }\n}\n", "import { Record } from \"@mail/core/common/record\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\n/** @typedef {{ thread?: import(\"models\").Thread }} ChatWindowData */\n\nexport class ChatWindow extends Record {\n    static id = \"thread\";\n    /** @type {Object<number, import(\"models\").ChatWindow} */\n    static records = {};\n    /** @returns {import(\"models\").ChatWindow} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").ChatWindow|import(\"models\").ChatWindow[]} */\n    static insert() {\n        return super.insert(...arguments);\n    }\n\n    thread = Record.one(\"Thread\");\n    autofocus = 0;\n    hidden = false;\n    /** Whether the chat window was created from the messaging menu */\n    fromMessagingMenu = false;\n    hubAsOpened = Record.one(\"ChatHub\", {\n        /** @this {import(\"models\").ChatWindow} */\n        onAdd() {\n            this.hubAsFolded = undefined;\n        },\n        /** @this {import(\"models\").ChatWindow} */\n        onDelete() {\n            if (!this.thread && !this.hubAsOpened) {\n                this.delete();\n            }\n        },\n    });\n    hubAsFolded = Record.one(\"ChatHub\", {\n        /** @this {import(\"models\").ChatWindow} */\n        onAdd() {\n            this.hubAsOpened = undefined;\n        },\n    });\n\n    get displayName() {\n        return this.thread?.displayName ?? _t(\"New message\");\n    }\n\n    get isOpen() {\n        return Boolean(this.hubAsOpened);\n    }\n\n    async close(options = {}) {\n        const { escape = false } = options;\n        const chatHub = this.store.chatHub;\n        const indexAsOpened = chatHub.opened.findIndex((w) => w.eq(this));\n        const thread = this.thread;\n        if (thread) {\n            thread.state = \"closed\";\n        }\n        await this._onClose(options);\n        this.delete();\n        if (escape && indexAsOpened !== -1 && chatHub.opened.length > 0) {\n            chatHub.opened[indexAsOpened === 0 ? 0 : indexAsOpened - 1].focus();\n        }\n    }\n\n    focus() {\n        this.autofocus++;\n    }\n\n    fold() {\n        if (!this.thread) {\n            return this.close();\n        }\n        this.store.chatHub.folded.delete(this);\n        this.store.chatHub.folded.unshift(this);\n        this.thread.state = \"folded\";\n        this.notifyState();\n    }\n\n    open({ notifyState = true } = {}) {\n        this.store.chatHub.opened.delete(this);\n        this.store.chatHub.opened.unshift(this);\n        if (this.thread) {\n            this.thread.state = \"open\";\n            if (notifyState) {\n                this.notifyState();\n            }\n        }\n        this.focus();\n    }\n\n    notifyState() {\n        if (\n            this.store.env.services.ui.isSmall ||\n            this.thread?.isTransient ||\n            !this.thread?.hasSelfAsMember\n        ) {\n            return;\n        }\n        if (this.thread?.model === \"discuss.channel\") {\n            this.thread.foldStateCount++;\n            return rpc(\n                \"/discuss/channel/fold\",\n                {\n                    channel_id: this.thread.id,\n                    state: this.thread.state,\n                    state_count: this.thread.foldStateCount,\n                },\n                { shadow: true }\n            );\n        }\n    }\n\n    async _onClose({ notifyState = true } = {}) {\n        if (notifyState) {\n            this.notifyState();\n        }\n    }\n}\n\nChatWindow.register();\n", "import { AttachmentList } from \"@mail/core/common/attachment_list\";\nimport { useAttachmentUploader } from \"@mail/core/common/attachment_uploader_hook\";\nimport { useDropzone } from \"@web/core/dropzone/dropzone_hook\";\nimport { Picker, usePicker } from \"@mail/core/common/picker\";\nimport { MessageConfirmDialog } from \"@mail/core/common/message_confirm_dialog\";\nimport { NavigableList } from \"@mail/core/common/navigable_list\";\nimport { useSuggestion } from \"@mail/core/common/suggestion_hook\";\nimport { prettifyMessageContent } from \"@mail/utils/common/format\";\nimport { useSelection } from \"@mail/utils/common/hooks\";\nimport { isDragSourceExternalFile } from \"@mail/utils/common/misc\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { isEventHandled, markEventHandled } from \"@web/core/utils/misc\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { useDebounced } from \"@web/core/utils/timing\";\n\nimport {\n    Component,\n    markup,\n    onMounted,\n    useChildSubEnv,\n    useEffect,\n    useRef,\n    useState,\n    useExternalListener,\n    toRaw,\n} from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\nimport { escape, sprintf } from \"@web/core/utils/strings\";\nimport { isDisplayStandalone, isIOS, isMobileOS } from \"@web/core/browser/feature_detection\";\n\nconst EDIT_CLICK_TYPE = {\n    CANCEL: \"cancel\",\n    SAVE: \"save\",\n};\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Composer} composer\n * @property {import(\"@mail/utils/common/hooks\").MessageToReplyTo} messageToReplyTo\n * @property {import(\"@mail/utils/common/hooks\").MessageEdition} [messageEdition]\n * @property {'compact'|'normal'|'extended'} [mode] default: 'normal'\n * @property {'message'|'note'|false} [type] default: false\n * @property {string} [placeholder]\n * @property {string} [className]\n * @property {function} [onDiscardCallback]\n * @property {function} [onPostCallback]\n * @property {number} [autofocus]\n * @property {import(\"@web/core/utils/hooks\").Ref} [dropzoneRef]\n * @extends {Component<Props, Env>}\n */\nexport class Composer extends Component {\n    static components = {\n        AttachmentList,\n        Picker,\n        FileUploader,\n        NavigableList,\n    };\n    static defaultProps = {\n        mode: \"normal\",\n        className: \"\",\n        sidebar: true,\n        showFullComposer: true,\n        allowUpload: true,\n    };\n    static props = [\n        \"composer\",\n        \"autofocus?\",\n        \"messageToReplyTo?\",\n        \"onCloseFullComposerCallback?\",\n        \"onDiscardCallback?\",\n        \"onPostCallback?\",\n        \"mode?\",\n        \"placeholder?\",\n        \"dropzoneRef?\",\n        \"messageEdition?\",\n        \"className?\",\n        \"sidebar?\",\n        \"type?\",\n        \"showFullComposer?\",\n        \"allowUpload?\",\n    ];\n    static template = \"mail.Composer\";\n\n    setup() {\n        super.setup();\n        this.isMobileOS = isMobileOS();\n        this.isIosPwa = isIOS() && isDisplayStandalone();\n        this.OR_PRESS_SEND_KEYBIND = markup(\n            _t(\"or press %(send_keybind)s\", {\n                send_keybind: this.sendKeybinds\n                    .map((key) => `<samp>${escape(key)}</samp>`)\n                    .join(\" + \"),\n            })\n        );\n        this.store = useState(useService(\"mail.store\"));\n        this.attachmentUploader = useAttachmentUploader(\n            this.thread ?? this.props.composer.message.thread,\n            { composer: this.props.composer }\n        );\n        this.ui = useState(useService(\"ui\"));\n        this.mainActionsRef = useRef(\"main-actions\");\n        this.ref = useRef(\"textarea\");\n        this.fakeTextarea = useRef(\"fakeTextarea\");\n        this.emojiButton = useRef(\"emoji-button\");\n        this.inputContainerRef = useRef(\"input-container\");\n        this.state = useState({\n            active: true,\n        });\n        this.selection = useSelection({\n            refName: \"textarea\",\n            model: this.props.composer.selection,\n            preserveOnClickAwayPredicate: async (ev) => {\n                // Let event be handled by bubbling handlers first.\n                await new Promise(setTimeout);\n                return (\n                    !this.isEventTrusted(ev) ||\n                    isEventHandled(ev, \"sidebar.openThread\") ||\n                    isEventHandled(ev, \"emoji.selectEmoji\") ||\n                    isEventHandled(ev, \"Composer.onClickAddEmoji\") ||\n                    isEventHandled(ev, \"composer.clickOnAddAttachment\") ||\n                    isEventHandled(ev, \"composer.selectSuggestion\")\n                );\n            },\n        });\n        this.suggestion = useSuggestion();\n        this.markEventHandled = markEventHandled;\n        this.onDropFile = this.onDropFile.bind(this);\n        this.saveContentDebounced = useDebounced(this.saveContent, 5000, {\n            execBeforeUnmount: true,\n        });\n        useExternalListener(window, \"beforeunload\", this.saveContent.bind(this));\n        if (this.props.dropzoneRef) {\n            useDropzone(\n                this.props.dropzoneRef,\n                this.onDropFile,\n                \"o-mail-Composer-dropzone\",\n                () => this.allowUpload\n            );\n        }\n        if (this.props.messageEdition) {\n            this.props.messageEdition.composerOfThread = this;\n        }\n        useChildSubEnv({\n            inComposer: true,\n        });\n        this.picker = usePicker(this.pickerSettings);\n        useEffect(\n            (focus) => {\n                if (focus && this.ref.el) {\n                    this.selection.restore();\n                    this.ref.el.focus();\n                }\n            },\n            () => [this.props.autofocus + this.props.composer.autofocus, this.props.placeholder]\n        );\n        useEffect(\n            (rThread, cThread) => {\n                if (cThread && cThread.eq(rThread)) {\n                    this.props.composer.autofocus++;\n                }\n            },\n            () => [this.props.messageToReplyTo?.thread, this.props.composer.thread]\n        );\n        useEffect(\n            () => {\n                if (this.fakeTextarea.el.scrollHeight) {\n                    this.ref.el.style.height = this.fakeTextarea.el.scrollHeight + \"px\";\n                }\n                this.saveContentDebounced();\n            },\n            () => [this.props.composer.text, this.ref.el]\n        );\n        useEffect(\n            () => {\n                if (!this.props.composer.forceCursorMove) {\n                    return;\n                }\n                this.selection.restore();\n                this.props.composer.forceCursorMove = false;\n            },\n            () => [this.props.composer.forceCursorMove]\n        );\n        onMounted(() => {\n            this.ref.el.scrollTo({ top: 0, behavior: \"instant\" });\n            if (!this.props.composer.text) {\n                this.restoreContent();\n            }\n        });\n    }\n\n    get pickerSettings() {\n        return {\n            anchor: this.props.mode === \"extended\" ? undefined : this.mainActionsRef,\n            buttons: [this.emojiButton],\n            close: () => {\n                if (!this.ui.isSmall) {\n                    this.props.composer.autofocus++;\n                }\n            },\n            pickers: { emoji: (emoji) => this.addEmoji(emoji) },\n            position:\n                this.props.mode === \"extended\"\n                    ? \"bottom-start\"\n                    : this.props.composer.message\n                    ? \"bottom-start\"\n                    : \"top-end\",\n            fixed: !this.props.composer.message,\n        };\n    }\n\n    get placeholder() {\n        if (this.props.placeholder) {\n            return this.props.placeholder;\n        }\n        if (this.thread) {\n            if (this.thread.channel_type === \"channel\") {\n                const threadName = this.thread.displayName;\n                if (this.thread.parent_channel_id) {\n                    return _t(`Message \"%(subChannelName)s\"`, {\n                        subChannelName: threadName,\n                    });\n                }\n                return _t(\"Message #%(threadName)s\u2026\", { threadName });\n            }\n            return _t(\"Message %(thread name)s\u2026\", { \"thread name\": this.thread.displayName });\n        }\n        return \"\";\n    }\n\n    onClickCancelOrSaveEditText(ev) {\n        const composer = toRaw(this.props.composer);\n        if (composer.message && ev.target.dataset?.type === EDIT_CLICK_TYPE.CANCEL) {\n            this.props.onDiscardCallback(ev);\n        }\n        if (composer.message && ev.target.dataset?.type === EDIT_CLICK_TYPE.SAVE) {\n            this.editMessage(ev);\n        }\n    }\n\n    get CANCEL_OR_SAVE_EDIT_TEXT() {\n        if (this.ui.isSmall) {\n            return markup(\n                sprintf(\n                    escape(\n                        _t(\n                            \"%(open_button)s%(icon)s%(open_em)sDiscard editing%(close_em)s%(close_button)s\"\n                        )\n                    ),\n                    {\n                        open_button: `<button class='btn px-1 py-0' data-type=\"${escape(\n                            EDIT_CLICK_TYPE.CANCEL\n                        )}\">`,\n                        close_button: \"</button>\",\n                        icon: `<i class='fa fa-times-circle pe-1' data-type=\"${escape(\n                            EDIT_CLICK_TYPE.CANCEL\n                        )}\"></i>`,\n                        open_em: `<em data-type=\"${escape(EDIT_CLICK_TYPE.CANCEL)}\">`,\n                        close_em: \"</em>\",\n                    }\n                )\n            );\n        } else {\n            const translation1 = _t(\n                \"%(open_samp)sEscape%(close_samp)s %(open_em)sto %(open_cancel)scancel%(close_cancel)s%(close_em)s, %(open_samp)sCTRL-Enter%(close_samp)s %(open_em)sto %(open_save)ssave%(close_save)s%(close_em)s\"\n            );\n            const translation2 = _t(\n                \"%(open_samp)sEscape%(close_samp)s %(open_em)sto %(open_cancel)scancel%(close_cancel)s%(close_em)s, %(open_samp)sEnter%(close_samp)s %(open_em)sto %(open_save)ssave%(close_save)s%(close_em)s\"\n            );\n            return markup(\n                sprintf(escape(this.props.mode === \"extended\" ? translation1 : translation2), {\n                    open_samp: \"<samp>\",\n                    close_samp: \"</samp>\",\n                    open_em: \"<em>\",\n                    close_em: \"</em>\",\n                    open_cancel: `<a role=\"button\" href=\"#\" data-type=\"${escape(\n                        EDIT_CLICK_TYPE.CANCEL\n                    )}\">`,\n                    close_cancel: \"</a>\",\n                    open_save: `<a role=\"button\" href=\"#\" data-type=\"${escape(\n                        EDIT_CLICK_TYPE.SAVE\n                    )}\">`,\n                    close_save: \"</a>\",\n                })\n            );\n        }\n    }\n\n    get SEND_TEXT() {\n        if (this.props.composer.message) {\n            return _t(\"Save editing\");\n        }\n        return this.props.type === \"note\" ? _t(\"Log\") : _t(\"Send\");\n    }\n\n    get sendKeybinds() {\n        return this.props.mode === \"extended\" ? [_t(\"CTRL\"), _t(\"Enter\")] : [_t(\"Enter\")];\n    }\n\n    get showComposerAvatar() {\n        return !this.compact && this.props.sidebar;\n    }\n\n    get thread() {\n        return this.props.messageToReplyTo?.message?.thread ?? this.props.composer.thread ?? null;\n    }\n\n    get allowUpload() {\n        return this.props.allowUpload;\n    }\n\n    get message() {\n        return this.props.composer.message ?? null;\n    }\n\n    get extraData() {\n        return this.thread.rpcParams;\n    }\n\n    get isSendButtonDisabled() {\n        const attachments = this.props.composer.attachments;\n        return (\n            !this.state.active ||\n            (!this.props.composer.text && attachments.length === 0) ||\n            attachments.some(({ uploading }) => Boolean(uploading))\n        );\n    }\n\n    get hasSendButtonNonEditing() {\n        return !this.extended;\n    }\n\n    get hasSuggestions() {\n        return Boolean(this.suggestion?.state.items);\n    }\n\n    get navigableListProps() {\n        const props = {\n            anchorRef: this.inputContainerRef.el,\n            position: this.env.inChatter ? \"bottom-fit\" : \"top-fit\",\n            onSelect: (ev, option) => {\n                this.suggestion.insert(option);\n                markEventHandled(ev, \"composer.selectSuggestion\");\n            },\n            isLoading: !!this.suggestion.search.term && this.suggestion.state.isFetching,\n            options: [],\n        };\n        if (!this.hasSuggestions) {\n            return props;\n        }\n        const suggestions = this.suggestion.state.items.suggestions;\n        switch (this.suggestion.state.items.type) {\n            case \"Partner\":\n                return {\n                    ...props,\n                    optionTemplate: \"mail.Composer.suggestionPartner\",\n                    options: suggestions.map((suggestion) => {\n                        if (suggestion.isSpecial) {\n                            return {\n                                ...suggestion,\n                                group: 1,\n                                optionTemplate: \"mail.Composer.suggestionSpecial\",\n                                classList: \"o-mail-Composer-suggestion\",\n                            };\n                        } else {\n                            return {\n                                label: suggestion.name,\n                                partner: suggestion,\n                                classList: \"o-mail-Composer-suggestion\",\n                            };\n                        }\n                    }),\n                };\n            case \"Thread\":\n                return {\n                    ...props,\n                    optionTemplate: \"mail.Composer.suggestionThread\",\n                    options: suggestions.map((suggestion) => {\n                        return {\n                            label: suggestion.parent_channel_id\n                                ? `${suggestion.parent_channel_id.displayName} > ${suggestion.displayName}`\n                                : suggestion.displayName,\n                            thread: suggestion,\n                            classList: \"o-mail-Composer-suggestion\",\n                        };\n                    }),\n                };\n            case \"ChannelCommand\":\n                return {\n                    ...props,\n                    optionTemplate: \"mail.Composer.suggestionChannelCommand\",\n                    options: suggestions.map((suggestion) => {\n                        return {\n                            label: suggestion.name,\n                            help: suggestion.help,\n                            classList: \"o-mail-Composer-suggestion\",\n                        };\n                    }),\n                };\n            case \"mail.canned.response\":\n                return {\n                    ...props,\n                    autoSelectFirst: false,\n                    hint: _t(\"Tab to select\"),\n                    optionTemplate: \"mail.Composer.suggestionCannedResponse\",\n                    options: suggestions.map((suggestion) => {\n                        return {\n                            cannedResponse: suggestion,\n                            source: suggestion.source,\n                            label: suggestion.substitution,\n                            classList: \"o-mail-Composer-suggestion\",\n                        };\n                    }),\n                };\n            default:\n                return props;\n        }\n    }\n\n    onDropFile(ev) {\n        if (isDragSourceExternalFile(ev.dataTransfer)) {\n            for (const file of ev.dataTransfer.files) {\n                this.attachmentUploader.uploadFile(file);\n            }\n        }\n    }\n\n    onCloseFullComposerCallback() {\n        if (this.props.onCloseFullComposerCallback) {\n            this.props.onCloseFullComposerCallback();\n        } else {\n            this.thread?.fetchNewMessages();\n        }\n    }\n\n    /**\n     * This doesn't work on firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1699743\n     */\n    onPaste(ev) {\n        if (!this.allowUpload) {\n            return;\n        }\n        if (!ev.clipboardData?.items) {\n            return;\n        }\n        if (ev.clipboardData.files.length === 0) {\n            return;\n        }\n        ev.preventDefault();\n        for (const file of ev.clipboardData.files) {\n            this.attachmentUploader.uploadFile(file);\n        }\n    }\n\n    onKeydown(ev) {\n        const composer = toRaw(this.props.composer);\n        switch (ev.key) {\n            case \"ArrowUp\":\n                if (this.props.messageEdition && composer.text === \"\") {\n                    const messageToEdit = composer.thread.lastEditableMessageOfSelf;\n                    if (messageToEdit) {\n                        this.props.messageEdition.editingMessage = messageToEdit;\n                    }\n                }\n                break;\n            case \"Enter\": {\n                if (isEventHandled(ev, \"NavigableList.select\") || !this.state.active) {\n                    ev.preventDefault();\n                    return;\n                }\n                const shouldPost = this.props.mode === \"extended\" ? ev.ctrlKey : !ev.shiftKey;\n                if (!shouldPost) {\n                    return;\n                }\n                ev.preventDefault(); // to prevent useless return\n                if (composer.message) {\n                    this.editMessage();\n                } else {\n                    this.sendMessage();\n                }\n                break;\n            }\n            case \"Escape\":\n                if (isEventHandled(ev, \"NavigableList.close\")) {\n                    return;\n                }\n                if (this.props.onDiscardCallback) {\n                    this.props.onDiscardCallback();\n                    markEventHandled(ev, \"Composer.discard\");\n                }\n                break;\n        }\n    }\n\n    onClickAddAttachment(ev) {\n        const composer = toRaw(this.props.composer);\n        markEventHandled(ev, \"composer.clickOnAddAttachment\");\n        composer.autofocus++;\n    }\n\n    async onClickFullComposer(ev) {\n        if (this.props.type !== \"note\") {\n            // auto-create partners of checked suggested partners\n            const newPartners = this.thread.suggestedRecipients.filter(\n                (recipient) => recipient.checked && !recipient.persona\n            );\n            if (newPartners.length !== 0) {\n                const recipientEmails = [];\n                const recipientAdditionalValues = {};\n                newPartners.forEach((recipient) => {\n                    recipientEmails.push(recipient.email);\n                    recipientAdditionalValues[recipient.email] = recipient.create_values || {};\n                });\n                const partners = await rpc(\"/mail/partner/from_email\", {\n                    emails: recipientEmails,\n                    additional_values: recipientAdditionalValues,\n                });\n                for (const index in partners) {\n                    const partnerData = partners[index];\n                    const persona = this.store.Persona.insert({ ...partnerData, type: \"partner\" });\n                    const email = recipientEmails[index];\n                    const recipient = this.thread.suggestedRecipients.find(\n                        (recipient) => recipient.email === email\n                    );\n                    Object.assign(recipient, { persona });\n                }\n            }\n        }\n        const attachmentIds = this.props.composer.attachments.map((attachment) => attachment.id);\n        const body = this.props.composer.text;\n        const validMentions = this.store.getMentionsFromText(body, {\n            mentionedChannels: this.props.composer.mentionedChannels,\n            mentionedPartners: this.props.composer.mentionedPartners,\n        });\n        const context = {\n            default_attachment_ids: attachmentIds,\n            default_body: await prettifyMessageContent(body, validMentions),\n            default_model: this.thread.model,\n            default_partner_ids:\n                this.props.type === \"note\"\n                    ? []\n                    : this.thread.suggestedRecipients\n                          .filter((recipient) => recipient.checked)\n                          .map((recipient) => recipient.persona.id),\n            default_res_ids: [this.thread.id],\n            default_subtype_xmlid: this.props.type === \"note\" ? \"mail.mt_note\" : \"mail.mt_comment\",\n            mail_post_autofollow: this.thread.hasWriteAccess,\n        };\n        const action = {\n            name: this.props.type === \"note\" ? _t(\"Log note\") : _t(\"Compose Email\"),\n            type: \"ir.actions.act_window\",\n            res_model: \"mail.compose.message\",\n            view_mode: \"form\",\n            views: [[false, \"form\"]],\n            target: \"new\",\n            context: context,\n        };\n        const options = {\n            onClose: (...args) => {\n                // args === [] : click on 'X' or press escape\n                // args === { special: true } : click on 'discard'\n                const accidentalDiscard = args.length === 0;\n                const isDiscard = accidentalDiscard || args[0]?.special;\n                // otherwise message is posted (args === [undefined])\n                if (!isDiscard && this.props.composer.thread.model === \"mail.box\") {\n                    this.notifySendFromMailbox();\n                }\n                if (accidentalDiscard) {\n                    const editor = document.querySelector(\n                        \".o_mail_composer_form_view .note-editable\"\n                    );\n                    const editorIsEmpty = !editor || !editor.innerText.replace(/^\\s*$/gm, \"\");\n                    if (!editorIsEmpty) {\n                        this.saveContent();\n                        this.restoreContent();\n                    }\n                } else {\n                    this.clear();\n                }\n                this.props.messageToReplyTo?.cancel();\n                this.onCloseFullComposerCallback();\n            },\n        };\n        await this.env.services.action.doAction(action, options);\n    }\n\n    clear() {\n        this.props.composer.clear();\n        browser.localStorage.removeItem(this.props.composer.localId);\n    }\n\n    notifySendFromMailbox() {\n        this.env.services.notification.add(_t('Message posted on \"%s\"', this.thread.displayName), {\n            type: \"info\",\n        });\n    }\n\n    onClickAddEmoji(ev) {\n        markEventHandled(ev, \"Composer.onClickAddEmoji\");\n    }\n\n    isEventTrusted(ev) {\n        // Allow patching during tests\n        return ev.isTrusted;\n    }\n\n    async processMessage(cb) {\n        const el = this.ref.el;\n        const attachments = this.props.composer.attachments;\n        if (attachments.some(({ uploading }) => uploading)) {\n            this.env.services.notification.add(_t(\"Please wait while the file is uploading.\"), {\n                type: \"warning\",\n            });\n        } else if (\n            this.props.composer.text.trim() ||\n            attachments.length > 0 ||\n            (this.message && this.message.attachment_ids.length > 0)\n        ) {\n            if (!this.state.active) {\n                return;\n            }\n            this.state.active = false;\n            await cb(this.props.composer.text);\n            if (this.props.onPostCallback) {\n                this.props.onPostCallback();\n            }\n            this.clear();\n            this.state.active = true;\n            el.focus();\n        }\n    }\n\n    async sendMessage() {\n        const composer = toRaw(this.props.composer);\n        if (composer.message) {\n            this.editMessage();\n            return;\n        }\n        await this.processMessage(async (value) => {\n            await this._sendMessage(value, this.postData, this.extraData);\n        });\n    }\n\n    get postData() {\n        const composer = toRaw(this.props.composer);\n        return {\n            attachments: composer.attachments || [],\n            isNote: this.props.type === \"note\",\n            mentionedChannels: composer.mentionedChannels || [],\n            mentionedPartners: composer.mentionedPartners || [],\n            cannedResponseIds: composer.cannedResponses.map((c) => c.id),\n            parentId: this.props.messageToReplyTo?.message?.id,\n        };\n    }\n\n    /**\n     * @typedef postData\n     * @property {import('@mail/attachments/attachment_model').Attachment[]} attachments\n     * @property {boolean} isNote\n     * @property {number} parentId\n     * @property {integer[]} mentionedChannelIds\n     * @property {integer[]} mentionedPartnerIds\n     */\n\n    /**\n     * @param {string} value message body\n     * @param {postData} postData Message meta data info\n     * @param {extraData} extraData Message extra meta data info needed by other modules\n     */\n    async _sendMessage(value, postData, extraData) {\n        const thread = toRaw(this.props.composer.thread);\n        const postThread = toRaw(this.thread);\n        const post = postThread.post.bind(postThread, value, postData, extraData);\n        if (postThread.model === \"discuss.channel\") {\n            // feature of (optimistic) temp message\n            post();\n        } else {\n            await post();\n        }\n        if (thread.model === \"mail.box\") {\n            this.notifySendFromMailbox();\n        }\n        this.suggestion?.clearRawMentions();\n        this.suggestion?.clearCannedResponses();\n        this.props.messageToReplyTo?.cancel();\n    }\n\n    async editMessage() {\n        const composer = toRaw(this.props.composer);\n        if (composer.text || composer.message.attachment_ids.length > 0) {\n            await this.processMessage(async (value) =>\n                composer.message.edit(value, composer.attachments, {\n                    mentionedChannels: composer.mentionedChannels,\n                    mentionedPartners: composer.mentionedPartners,\n                })\n            );\n        } else {\n            this.env.services.dialog.add(MessageConfirmDialog, {\n                message: composer.message,\n                onConfirm: () => this.message.remove(),\n                prompt: _t(\"Are you sure you want to delete this message?\"),\n            });\n        }\n        this.suggestion?.clearRawMentions();\n    }\n\n    addEmoji(str) {\n        const composer = toRaw(this.props.composer);\n        const text = composer.text;\n        const firstPart = text.slice(0, composer.selection.start);\n        const secondPart = text.slice(composer.selection.end, text.length);\n        composer.text = firstPart + str + secondPart;\n        this.selection.moveCursor((firstPart + str).length);\n        if (!this.ui.isSmall) {\n            composer.autofocus++;\n        }\n    }\n\n    onFocusin() {\n        const composer = toRaw(this.props.composer);\n        composer.isFocused = true;\n        composer.thread?.markAsRead();\n    }\n\n    onFocusout(ev) {\n        if (\n            [EDIT_CLICK_TYPE.CANCEL, EDIT_CLICK_TYPE.SAVE].includes(ev.relatedTarget?.dataset?.type)\n        ) {\n            // Edit or Save most likely clicked: early return as to not re-render (which prevents click)\n            return;\n        }\n        this.props.composer.isFocused = false;\n    }\n\n    saveContent() {\n        const composer = toRaw(this.props.composer);\n        const fullComposerContent =\n            document\n                .querySelector(\".o_mail_composer_form_view .note-editable\")\n                ?.innerText.replace(/(\\t|\\n)+/g, \"\\n\") ?? composer.text;\n        browser.localStorage.setItem(composer.localId, fullComposerContent);\n    }\n\n    restoreContent() {\n        const composer = toRaw(this.props.composer);\n        const fullComposerContent = browser.localStorage.getItem(composer.localId);\n        if (fullComposerContent) {\n            composer.text = fullComposerContent;\n        }\n    }\n}\n", "import { OR, Record } from \"@mail/core/common/record\";\n\nexport class Composer extends Record {\n    static id = OR(\"thread\", \"message\");\n    /** @returns {import(\"models\").Composer} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Composer|import(\"models\").Composer[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    clear() {\n        this.attachments.length = 0;\n        this.text = \"\";\n        Object.assign(this.selection, {\n            start: 0,\n            end: 0,\n            direction: \"none\",\n        });\n    }\n\n    attachments = Record.many(\"Attachment\");\n    message = Record.one(\"Message\");\n    mentionedPartners = Record.many(\"Persona\");\n    mentionedChannels = Record.many(\"Thread\");\n    cannedResponses = Record.many(\"mail.canned.response\");\n    text = \"\";\n    thread = Record.one(\"Thread\");\n    /** @type {{ start: number, end: number, direction: \"forward\" | \"backward\" | \"none\"}}*/\n    selection = {\n        start: 0,\n        end: 0,\n        direction: \"none\",\n    };\n    /** @type {boolean} */\n    forceCursorMove;\n    isFocused = false;\n    autofocus = 0;\n}\n\nComposer.register();\n", "import { Component } from \"@odoo/owl\";\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class CountryFlag extends Component {\n    static props = [\"country\", \"class?\"];\n    static template = \"mail.CountryFlag\";\n}\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class Country extends Record {\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").Country>} */\n    static records = {};\n    /** @returns {import(\"models\").Country} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Country|import(\"models\").Country[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n    /** @type {string} */\n    code;\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    name;\n\n    get flagUrl() {\n        return `/base/static/img/country_flags/${encodeURIComponent(this.code.toLowerCase())}.png`;\n    }\n}\n\nCountry.register();\n", "import { Component } from \"@odoo/owl\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\n\n/**\n * @typedef {Object} Props\n * @property {string} date\n * @property {string} [className]\n */\nexport class DateSection extends Component {\n    static template = \"mail.DateSection\";\n    static props = [\"date\", \"className?\"];\n\n    get isMobileOS() {\n        return isMobileOS();\n    }\n}\n", "import { registry } from \"@web/core/registry\";\n\nexport const discussComponentRegistry = registry.category(\"discuss.component\");\n", "import { Component, xml } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { EMOJI_PICKER_PROPS, EmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\n\nexport class EmojiPickerMobile extends Component {\n    static components = { Dialog, EmojiPicker };\n    static props = EMOJI_PICKER_PROPS;\n    static template = xml`\n        <Dialog size=\"'lg'\" header=\"false\" footer=\"false\" contentClass=\"'o-discuss-mobileContextMenu d-flex position-absolute bottom-0 rounded-0 h-50 bg-100'\">\n            <EmojiPicker t-props=\"props\"/>\n        </Dialog>\n    `;\n}\n", "import { Record } from \"@mail/core/common/record\";\nimport { markRaw } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class Failure extends Record {\n    static nextId = markRaw({ value: 1 });\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").Failure>} */\n    static records = {};\n    /** @returns {import(\"models\").Failure} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Failure|import(\"models\").Failure[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    notifications = Record.many(\"Notification\", {\n        /** @this {import(\"models\").Failure} */\n        onUpdate() {\n            if (this.notifications.length === 0) {\n                this.delete();\n            } else {\n                this.store.failures.add(this);\n            }\n        },\n    });\n    get modelName() {\n        return this.notifications?.[0]?.message?.thread?.modelName;\n    }\n    get resModel() {\n        return this.notifications?.[0]?.message?.thread?.model;\n    }\n    get resIds() {\n        return new Set([\n            ...this.notifications.map((notif) => notif.message?.thread?.id).filter((id) => !!id),\n        ]);\n    }\n    lastMessage = Record.one(\"Message\", {\n        /** @this {import(\"models\").Failure} */\n        compute() {\n            let lastMsg = this.notifications[0]?.message;\n            for (const notification of this.notifications) {\n                if (lastMsg?.id < notification.message?.id) {\n                    lastMsg = notification.message;\n                }\n            }\n            return lastMsg;\n        },\n    });\n    /** @type {'sms' | 'email'} */\n    get type() {\n        return this.notifications?.[0]?.notification_type;\n    }\n    get status() {\n        return this.notifications?.[0]?.notification_status;\n    }\n\n    get iconSrc() {\n        return \"/mail/static/src/img/smiley/mailfailure.jpg\";\n    }\n\n    get body() {\n        return _t(\"An error occurred when sending an email\");\n    }\n\n    get datetime() {\n        return this.lastMessage?.datetime;\n    }\n}\n\nFailure.register();\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class Follower extends Record {\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").Follower>} */\n    static records = {};\n    /** @returns {import(\"models\").Follower} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Follower|import(\"models\").Follower[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    thread = Record.one(\"Thread\");\n    /** @type {number} */\n    id;\n    /** @type {boolean} */\n    is_active;\n    partner = Record.one(\"Persona\");\n\n    /** @returns {boolean} */\n    get isEditable() {\n        const hasWriteAccess = this.thread ? this.thread.hasWriteAccess : false;\n        return this.partner.eq(this.store.self) ? this.thread.hasReadAccess : hasWriteAccess;\n    }\n\n    async remove() {\n        await this.store.env.services.orm.call(this.thread.model, \"message_unsubscribe\", [\n            [this.thread.id],\n            [this.partner.id],\n        ]);\n        this.delete();\n    }\n\n    removeRecipient() {\n        this.thread.recipients.delete(this);\n    }\n}\n\nFollower.register();\n", "import { Component } from \"@odoo/owl\";\nimport { Typing } from \"@mail/discuss/typing/common/typing\";\n\nexport class ImStatus extends Component {\n    static props = [\"persona?\", \"className?\", \"style?\", \"member?\", \"size?\"];\n    static template = \"mail.ImStatus\";\n    static defaultProps = { className: \"\", style: \"\", size: \"lg\" };\n    static components = { Typing };\n\n    get persona() {\n        return this.props.persona ?? this.props.member?.persona;\n    }\n}\n", "/* @odoo-module */\n\nimport { AWAY_DELAY, imStatusService } from \"@bus/im_status_service\";\nimport { patch } from \"@web/core/utils/patch\";\n\nexport const imStatusServicePatch = {\n    start(env, services) {\n        const { bus_service, presence } = services;\n        const API = super.start(env, services);\n\n        bus_service.subscribe(\n            \"bus.bus/im_status_updated\",\n            ({ im_status, partner_id, guest_id }) => {\n                const store = env.services[\"mail.store\"];\n                if (!store) {\n                    return;\n                }\n                const persona = store.Persona.get({\n                    type: partner_id ? \"partner\" : \"guest\",\n                    id: partner_id ?? guest_id,\n                });\n                if (!persona) {\n                    return; // Do not store unknown persona's status\n                }\n                persona.im_status = im_status;\n                if (persona.type !== \"guest\" || persona.notEq(store.self)) {\n                    return; // Partners are already handled by the original service\n                }\n                const isOnline = presence.getInactivityPeriod() < AWAY_DELAY;\n                if ((im_status === \"away\" && isOnline) || im_status === \"offline\") {\n                    this.updateBusPresence();\n                }\n            }\n        );\n        return API;\n    },\n};\nexport const unpatchImStatusService = patch(imStatusService, imStatusServicePatch);\n", "import { LinkPreviewConfirmDelete } from \"@mail/core/common/link_preview_confirm_delete\";\n\nimport { Component } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").LinkPreview} linkPreview\n * @property {boolean} [deletable]\n * @extends {Component<Props, Env>}\n */\nexport class LinkPreview extends Component {\n    static template = \"mail.LinkPreview\";\n    static props = [\"linkPreview\", \"deletable\"];\n    static components = {};\n\n    setup() {\n        super.setup();\n        this.dialogService = useService(\"dialog\");\n    }\n\n    onClick() {\n        this.dialogService.add(LinkPreviewConfirmDelete, {\n            linkPreview: this.props.linkPreview,\n            LinkPreview,\n        });\n    }\n\n    onImageLoaded() {\n        this.env.onImageLoaded?.();\n    }\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").LinkPreview} linkPreview\n * @property {function} close\n * @property {Component} LinkPreviewListComponent\n * @extends {Component<Props, Env>}\n */\nexport class LinkPreviewConfirmDelete extends Component {\n    static components = { Dialog };\n    static props = [\"linkPreview\", \"close\", \"LinkPreview\"];\n    static template = \"mail.LinkPreviewConfirmDelete\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n    }\n\n    get message() {\n        return this.props.linkPreview.message;\n    }\n\n    onClickOk() {\n        rpc(\n            \"/mail/link_preview/hide\",\n            { link_preview_ids: [this.props.linkPreview.id] },\n            { silent: true }\n        );\n        this.props.close();\n    }\n\n    onClickDeleteAll() {\n        rpc(\n            \"/mail/link_preview/hide\",\n            { link_preview_ids: this.message.linkPreviews.map((lp) => lp.id) },\n            { silent: true }\n        );\n        this.props.close();\n    }\n\n    onClickCancel() {\n        this.props.close();\n    }\n}\n", "import { LinkPreview } from \"@mail/core/common/link_preview\";\n\nimport { Component } from \"@odoo/owl\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").LinkPreview[]} linkPreviews\n * @property {boolean} [deletable]\n * @extends {Component<Props, Env>}\n */\nexport class LinkPreviewList extends Component {\n    static template = \"mail.LinkPreviewList\";\n    static props = [\"linkPreviews\", \"deletable?\"];\n    static defaultProps = {\n        deletable: false,\n    };\n    static components = { LinkPreview };\n}\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class LinkPreview extends Record {\n    static id = \"id\";\n    /** @returns {import(\"models\").LinkPreview} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").LinkPreview|import(\"models\").LinkPreview[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    /** @type {number} */\n    id;\n    message = Record.one(\"Message\", { inverse: \"linkPreviews\" });\n    /** @type {string} */\n    image_mimetype;\n    /** @type {string} */\n    og_description;\n    /** @type {string} */\n    og_image;\n    /** @type {string} */\n    og_mimetype;\n    /** @type {string} */\n    og_title;\n    /** @type {string} */\n    og_type;\n    /** @type {string} */\n    og_site_name;\n    /** @type {string} */\n    source_url;\n\n    get imageUrl() {\n        return this.og_image ? this.og_image : this.source_url;\n    }\n\n    get isImage() {\n        return Boolean(this.image_mimetype || this.og_mimetype === \"image/gif\");\n    }\n\n    get isVideo() {\n        return Boolean(!this.isImage && this.og_type && this.og_type.startsWith(\"video\"));\n    }\n\n    get isCard() {\n        return !this.isImage && !this.isVideo;\n    }\n}\n\nLinkPreview.register();\n", "import { reactive } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\n\nexport class MailCoreCommon {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    constructor(env, services) {\n        this.env = env;\n        this.busService = services.bus_service;\n        this.store = services[\"mail.store\"];\n    }\n\n    setup() {\n        this.busService.subscribe(\"ir.attachment/delete\", (payload) => {\n            const { id: attachmentId, message: messageData } = payload;\n            if (messageData) {\n                this.store.Message.insert(messageData);\n            }\n            const attachment = this.store.Attachment.get(attachmentId);\n            attachment?.delete();\n        });\n        this.busService.subscribe(\"mail.message/delete\", (payload, { id: notifId }) => {\n            for (const messageId of payload.message_ids) {\n                const message = this.store.Message.get(messageId);\n                if (!message) {\n                    continue;\n                }\n                this.env.bus.trigger(\"mail.message/delete\", { message, notifId });\n                message.delete();\n            }\n        });\n        this.busService.subscribe(\"mail.message/toggle_star\", (payload, metadata) =>\n            this._handleNotificationToggleStar(payload, metadata)\n        );\n        this.busService.subscribe(\"res.users.settings\", (payload) => {\n            if (payload) {\n                this.store.settings.update(payload);\n            }\n        });\n        this.busService.subscribe(\"mail.record/insert\", (payload) => {\n            this.store.insert(payload, { html: true });\n        });\n    }\n\n    _handleNotificationToggleStar(payload, metadata) {\n        const { message_ids: messageIds, starred } = payload;\n        this.store.Message.insert(messageIds.map((id) => ({ id, starred })));\n    }\n}\n\nexport const mailCoreCommon = {\n    dependencies: [\"bus_service\", \"mail.store\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    start(env, services) {\n        const mailCoreCommon = reactive(new MailCoreCommon(env, services));\n        mailCoreCommon.setup();\n        return mailCoreCommon;\n    },\n};\n\nregistry.category(\"services\").add(\"mail.core.common\", mailCoreCommon);\n", "import { registry } from \"@web/core/registry\";\nimport { App } from \"@odoo/owl\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport const mailPopoutService = {\n    start(env) {\n        let externalWindow;\n        let beforeFn;\n        let afterFn;\n        let app;\n\n        /**\n         * Reset the external window to its initial state:\n         * - Reset the external window header from main window (for appropriate title and other meta data)\n         * - clear the external window's document body\n         * - destroy the current app mounted on the window\n         */\n        function reset() {\n            if (externalWindow) {\n                externalWindow.document.head.innerHTML = \"\";\n                externalWindow.document.write(window.document.head.outerHTML);\n                externalWindow.document.body = externalWindow.document.createElement(\"body\");\n            }\n            if (app) {\n                app.destroy();\n                app = null;\n            }\n        }\n\n        /**\n         * Poll the external window to detect when it is closed.\n         * the afterPopout hook (afterFn) is then called after the window is closed\n         */\n        async function pollClosedWindow() {\n            while (externalWindow) {\n                await new Promise((r) => setTimeout(r, 1000));\n                if (externalWindow.closed) {\n                    externalWindow = null;\n                    afterFn();\n                }\n            }\n        }\n\n        /**\n         * This function registers hooks (before/after the window popout)\n         * @param {Function} beforePopout: this function is called before the component is initially mounted on the external window.\n         * @param {Function} afterPopout: this function is called after the external window is closed.\n         */\n        function addHooks(beforePopout = () => {}, afterPopout = () => {}) {\n            beforeFn = beforePopout;\n            afterFn = afterPopout;\n        }\n\n        /**\n         * Mounts the passed component (with its props) on an external window.\n         * If the external window does not exist, it is created.\n         * @param {class} component: The component to be mounted.\n         * @param {Props} props: The props of the component.\n         * @returns {Window} The external window\n         */\n        function popout(component, props) {\n            if (!externalWindow || externalWindow.closed) {\n                externalWindow = browser.open(\"about:blank\", \"_blank\", \"popup=yes\");\n                window.addEventListener(\"beforeunload\", () => {\n                    if (externalWindow && !externalWindow.closed) {\n                        externalWindow.close();\n                    }\n                });\n                pollClosedWindow();\n            }\n\n            beforeFn();\n            reset();\n            app = new App(component, {\n                name: \"Popout\",\n                env,\n                props,\n                getTemplate,\n            });\n            app.mount(externalWindow.document.body);\n            return externalWindow;\n        }\n\n        return {\n            get externalWindow() {\n                return externalWindow && externalWindow.closed ? null : externalWindow;\n            },\n            popout,\n            reset,\n            addHooks,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"mail.popout\", mailPopoutService);\n", "import { AttachmentList } from \"@mail/core/common/attachment_list\";\nimport { Composer } from \"@mail/core/common/composer\";\nimport { ImStatus } from \"@mail/core/common/im_status\";\nimport { LinkPreviewList } from \"@mail/core/common/link_preview_list\";\nimport { MessageInReply } from \"@mail/core/common/message_in_reply\";\nimport { MessageNotificationPopover } from \"@mail/core/common/message_notification_popover\";\nimport { MessageReactionMenu } from \"@mail/core/common/message_reaction_menu\";\nimport { MessageReactions } from \"@mail/core/common/message_reactions\";\nimport { MessageSeenIndicator } from \"@mail/core/common/message_seen_indicator\";\nimport { RelativeTime } from \"@mail/core/common/relative_time\";\nimport { htmlToTextContentInline } from \"@mail/utils/common/format\";\nimport { isEventHandled, markEventHandled } from \"@web/core/utils/misc\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nimport {\n    Component,\n    markup,\n    onMounted,\n    onPatched,\n    onWillDestroy,\n    onWillUpdateProps,\n    toRaw,\n    useChildSubEnv,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { ActionSwiper } from \"@web/core/action_swiper/action_swiper\";\nimport { hasTouch, isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { url } from \"@web/core/utils/urls\";\nimport { messageActionsRegistry, useMessageActions } from \"./message_actions\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { MessageActionMenuMobile } from \"./message_action_menu_mobile\";\nimport { discussComponentRegistry } from \"./discuss_component_registry\";\n\n/**\n * @typedef {Object} Props\n * @property {boolean} [hasActions=true]\n * @property {boolean} [highlighted]\n * @property {function} [onParentMessageClick]\n * @property {import(\"models\").Message} message\n * @property {import(\"@mail/utils/common/hooks\").MessageToReplyTo} [messageToReplyTo]\n * @property {boolean} [squashed]\n * @property {import(\"models\").Thread} [thread]\n * @property {ReturnType<import('@mail/core/common/message_search_hook').useMessageSearch>} [messageSearch]\n * @property {String} [className]\n * @extends {Component<Props, Env>}\n */\nexport class Message extends Component {\n    // This is the darken version of #71639e\n    static SHADOW_LINK_COLOR = \"#66598f\";\n    static SHADOW_HIGHLIGHT_COLOR = \"#e99d00bf\";\n    static SHADOW_LINK_HOVER_COLOR = \"#564b79\";\n    static components = {\n        ActionSwiper,\n        AttachmentList,\n        Composer,\n        Dropdown,\n        DropdownItem,\n        LinkPreviewList,\n        MessageInReply,\n        MessageReactions,\n        MessageSeenIndicator,\n        ImStatus,\n        Popover: MessageNotificationPopover,\n        RelativeTime,\n    };\n    static defaultProps = {\n        hasActions: true,\n        isInChatWindow: false,\n        showDates: true,\n    };\n    static props = [\n        \"asCard?\",\n        \"registerMessageRef?\",\n        \"hasActions?\",\n        \"isInChatWindow?\",\n        \"onParentMessageClick?\",\n        \"message\",\n        \"messageEdition?\",\n        \"messageToReplyTo?\",\n        \"previousMessage?\",\n        \"squashed?\",\n        \"thread?\",\n        \"messageSearch?\",\n        \"className?\",\n        \"showDates?\",\n        \"isFirstMessage?\",\n    ];\n    static template = \"mail.Message\";\n\n    setup() {\n        super.setup();\n        this.escape = escape;\n        this.popover = usePopover(this.constructor.components.Popover, { position: \"top\" });\n        this.state = useState({\n            isEditing: false,\n            isHovered: false,\n            isClicked: false,\n            expandOptions: false,\n            emailHeaderOpen: false,\n            showTranslation: false,\n            actionMenuMobileOpen: false,\n        });\n        /** @type {ShadowRoot} */\n        this.shadowRoot;\n        this.root = useRef(\"root\");\n        onWillUpdateProps((nextProps) => {\n            this.props.registerMessageRef?.(this.props.message, null);\n        });\n        onMounted(() => this.props.registerMessageRef?.(this.props.message, this.root));\n        onPatched(() => this.props.registerMessageRef?.(this.props.message, this.root));\n        onWillDestroy(() => this.props.registerMessageRef?.(this.props.message, null));\n        this.hasTouch = hasTouch;\n        this.messageBody = useRef(\"body\");\n        this.messageActions = useMessageActions();\n        this.store = useState(useService(\"mail.store\"));\n        this.shadowBody = useRef(\"shadowBody\");\n        this.dialog = useService(\"dialog\");\n        this.ui = useState(useService(\"ui\"));\n        this.openReactionMenu = this.openReactionMenu.bind(this);\n        this.optionsDropdown = useDropdownState();\n        useChildSubEnv({\n            message: this.props.message,\n            alignedRight: this.isAlignedRight,\n        });\n        useEffect(\n            (editingMessage) => {\n                if (this.props.message.eq(editingMessage)) {\n                    messageActionsRegistry.get(\"edit\").onClick(this);\n                }\n            },\n            () => [this.props.messageEdition?.editingMessage]\n        );\n        onMounted(() => {\n            if (this.shadowBody.el) {\n                this.shadowRoot = this.shadowBody.el.attachShadow({ mode: \"open\" });\n                const color = cookie.get(\"color_scheme\") === \"dark\" ? \"white\" : \"black\";\n                const shadowStyle = document.createElement(\"style\");\n                shadowStyle.innerHTML = `\n                    * {\n                        background-color: transparent !important;\n                        color: ${color} !important;\n                    }\n                    a, a * {\n                        color: ${this.constructor.SHADOW_LINK_COLOR} !important;\n                    }\n                    a:hover, a *:hover {\n                        color: ${this.constructor.SHADOW_LINK_HOVER_COLOR} !important;\n                    }\n                    .o-mail-Message-searchHighlight {\n                        background: ${this.constructor.SHADOW_HIGHLIGHT_COLOR} !important;\n                    }\n                `;\n                if (cookie.get(\"color_scheme\") === \"dark\") {\n                    this.shadowRoot.appendChild(shadowStyle);\n                }\n            }\n        });\n        useEffect(\n            () => {\n                if (this.messageBody.el) {\n                    this.prepareMessageBody(this.messageBody.el);\n                }\n                if (this.shadowBody.el) {\n                    const bodyEl = document.createElement(\"span\");\n                    bodyEl.innerHTML = this.state.showTranslation\n                        ? this.message.translationValue\n                        : this.props.messageSearch?.highlight(this.message.body) ??\n                          this.message.body;\n                    this.prepareMessageBody(bodyEl);\n                    this.shadowRoot.appendChild(bodyEl);\n                    return () => {\n                        this.shadowRoot.removeChild(bodyEl);\n                    };\n                }\n            },\n            () => [\n                this.state.showTranslation,\n                this.message.translationValue,\n                this.props.messageSearch?.searchTerm,\n                this.message.body,\n            ]\n        );\n    }\n\n    get attClass() {\n        return {\n            [this.props.className]: true,\n            \"o-card p-2 mt-2\": this.props.asCard,\n            \"pt-1\": !this.props.asCard,\n            \"o-selfAuthored\": this.message.isSelfAuthored && !this.env.messageCard,\n            \"o-selected\": this.props.messageToReplyTo?.isSelected(\n                this.props.thread,\n                this.props.message\n            ),\n            \"o-squashed\": this.props.squashed,\n            \"mt-1\":\n                !this.props.squashed &&\n                this.props.thread &&\n                !this.env.messageCard &&\n                !this.props.asCard,\n            \"px-2\": this.props.isInChatWindow,\n            \"opacity-50\": this.props.messageToReplyTo?.isNotSelected(\n                this.props.thread,\n                this.props.message\n            ),\n            \"o-actionMenuMobileOpen\": this.state.actionMenuMobileOpen,\n            \"o-editing\": this.state.isEditing,\n        };\n    }\n\n    get authorAvatarAttClass() {\n        return {\n            o_object_fit_contain: this.props.message.author?.is_company,\n            o_object_fit_cover: !this.props.message.author?.is_company,\n        };\n    }\n\n    get authorName() {\n        if (this.message.author) {\n            return this.message.author.name;\n        }\n        return this.message.email_from;\n    }\n\n    get authorAvatarUrl() {\n        if (\n            this.message.message_type &&\n            this.message.message_type.includes(\"email\") &&\n            ![\"partner\", \"guest\"].includes(this.message.author?.type)\n        ) {\n            return url(\"/mail/static/src/img/email_icon.png\");\n        }\n\n        if (this.message.author) {\n            return this.message.author.avatarUrl;\n        }\n\n        return this.store.DEFAULT_AVATAR;\n    }\n\n    get expandText() {\n        return _t(\"Expand\");\n    }\n\n    get message() {\n        return this.props.message;\n    }\n\n    /** Max amount of quick actions, including \"...\" */\n    get quickActionCount() {\n        return this.env.inChatter ? 3 : this.env.inChatWindow ? 2 : 4;\n    }\n\n    get showSeenIndicator() {\n        return this.props.message.isSelfAuthored && this.props.thread?.hasSeenFeature;\n    }\n\n    get showSubtypeDescription() {\n        return (\n            this.message.subtype_description &&\n            this.message.subtype_description.toLowerCase() !==\n                htmlToTextContentInline(this.message.body || \"\").toLowerCase()\n        );\n    }\n\n    get messageTypeText() {\n        if (this.props.message.message_type === \"notification\") {\n            return _t(\"System notification\");\n        }\n        if (this.props.message.message_type === \"auto_comment\") {\n            return _t(\"Automated message\");\n        }\n        if (\n            !this.props.message.is_discussion &&\n            this.props.message.message_type !== \"user_notification\"\n        ) {\n            return _t(\"Note\");\n        }\n        return _t(\"Message\");\n    }\n\n    get isActive() {\n        return (\n            this.state.isHovered ||\n            this.state.isClicked ||\n            this.emojiPicker?.isOpen ||\n            this.optionsDropdown.isOpen\n        );\n    }\n\n    get isAlignedRight() {\n        return Boolean(this.env.inChatWindow && this.props.message.isSelfAuthored);\n    }\n\n    get isMobileOS() {\n        return isMobileOS();\n    }\n\n    get isPersistentMessageFromAnotherThread() {\n        return !this.isOriginThread && !this.message.is_transient && this.message.thread;\n    }\n\n    get isOriginThread() {\n        if (!this.props.thread) {\n            return false;\n        }\n        return this.props.thread.eq(this.message.thread);\n    }\n\n    get translatedFromText() {\n        return _t(\"(Translated from: %(language)s)\", { language: this.message.translationSource });\n    }\n\n    get translationFailureText() {\n        return _t(\"(Translation Failure: %(error)s)\", { error: this.message.translationErrors });\n    }\n\n    onMouseenter() {\n        this.state.isHovered = true;\n    }\n\n    onMouseleave() {\n        this.state.isHovered = false;\n        this.state.isClicked = null;\n    }\n\n    /**\n     * @returns {boolean}\n     */\n    get shouldDisplayAuthorName() {\n        if (!this.env.inChatWindow) {\n            return true;\n        }\n        if (this.message.isSelfAuthored) {\n            return false;\n        }\n        if (this.props.thread.channel_type === \"chat\") {\n            return false;\n        }\n        return true;\n    }\n\n    async onClickAttachmentUnlink(attachment) {\n        await toRaw(attachment).remove();\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    async onClick(ev) {\n        if (this.store.handleClickOnLink(ev, this.props.thread)) {\n            return;\n        }\n        if (\n            !isEventHandled(ev, \"Message.ClickAuthor\") &&\n            !isEventHandled(ev, \"Message.ClickFailure\")\n        ) {\n            if (this.state.isClicked) {\n                this.state.isClicked = false;\n            } else {\n                this.state.isClicked = true;\n                document.body.addEventListener(\n                    \"click\",\n                    () => {\n                        this.state.isClicked = false;\n                    },\n                    { capture: true, once: true }\n                );\n            }\n        }\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    async onClickNotificationMessage(ev) {\n        this.store.handleClickOnLink(ev, this.props.thread);\n        const { oeType, oeId } = ev.target.dataset;\n        if (oeType === \"highlight\") {\n            await this.env.messageHighlight?.highlightMessage(\n                this.store.Message.insert({\n                    id: Number(oeId),\n                    res_id: this.props.thread.id,\n                    model: this.props.thread.model,\n                }),\n                this.props.thread\n            );\n        }\n    }\n\n    /** @param {HTMLElement} bodyEl */\n    prepareMessageBody(bodyEl) {\n        if (!bodyEl) {\n            return;\n        }\n        const linkEls = bodyEl.querySelectorAll(\".o_channel_redirect\");\n        for (const linkEl of linkEls) {\n            const text = linkEl.textContent.substring(1); // remove '#' prefix\n            const icon = linkEl.classList.contains(\"o_channel_redirect_asThread\")\n                ? \"fa fa-comments-o\"\n                : \"fa fa-hashtag\";\n            const iconEl = renderToElement(\"mail.Message.mentionedChannelIcon\", { icon });\n            linkEl.replaceChildren(iconEl);\n            linkEl.insertAdjacentText(\"beforeend\", ` ${text}`);\n        }\n    }\n\n    getAuthorAttClass() {\n        return { \"opacity-50\": this.message.isPending };\n    }\n\n    getAvatarContainerAttClass() {\n        return {\n            \"opacity-50\": this.message.isPending,\n            \"o-inChatWindow\": this.env.inChatWindow,\n        };\n    }\n\n    exitEditMode() {\n        const message = toRaw(this.props.message);\n        this.props.messageEdition?.exitEditMode();\n        message.composer = undefined;\n        this.state.isEditing = false;\n    }\n\n    onClickNotification(ev) {\n        const message = toRaw(this.message);\n        if (message.failureNotifications.length > 0) {\n            this.onClickFailure(ev);\n        } else {\n            this.popover.open(ev.target, { message });\n        }\n    }\n\n    onClickFailure(ev) {\n        const message = toRaw(this.message);\n        markEventHandled(ev, \"Message.ClickFailure\");\n        this.env.services.action.doAction(\"mail.mail_resend_message_action\", {\n            additionalContext: {\n                mail_message_to_resend: message.id,\n            },\n        });\n    }\n\n    /** @param {MouseEvent} [ev] */\n    openMobileActions(ev) {\n        if (!isMobileOS()) {\n            return;\n        }\n        ev?.stopPropagation();\n        this.state.actionMenuMobileOpen = true;\n        this.dialog.add(\n            MessageActionMenuMobile,\n            {\n                message: this.props.message,\n                thread: this.props.thread,\n                isFirstMessage: this.props.isFirstMessage,\n                messageToReplyTo: this.props.messageToReplyTo,\n                openReactionMenu: () => this.openReactionMenu(),\n                state: this.state,\n            },\n            { context: this, onClose: () => (this.state.actionMenuMobileOpen = false) }\n        );\n    }\n\n    openReactionMenu(reaction) {\n        const message = toRaw(this.props.message);\n        this.dialog.add(\n            MessageReactionMenu,\n            { message, initialReaction: reaction },\n            { context: this }\n        );\n    }\n\n    async onClickToggleTranslation() {\n        const message = toRaw(this.message);\n        if (!message.translationValue) {\n            const { error, lang_name, body } = await rpc(\"/mail/message/translate\", {\n                message_id: message.id,\n            });\n            message.translationValue = body && markup(body);\n            message.translationSource = lang_name;\n            message.translationErrors = error;\n        }\n        this.state.showTranslation =\n            !this.state.showTranslation && Boolean(message.translationValue);\n    }\n}\n\ndiscussComponentRegistry.add(\"Message\", Message);\n", "import { Component, onMounted, onWillUnmount, useState } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useMessageActions } from \"./message_actions\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\n\nexport class MessageActionMenuMobile extends Component {\n    static components = { Dialog };\n    static props = [\n        \"message\",\n        \"close?\",\n        \"thread?\",\n        \"isFirstMessage?\",\n        \"messageToReplyTo?\",\n        \"openReactionMenu?\",\n        \"state\",\n    ];\n    static template = \"mail.MessageActionMenuMobile\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.modalRef = useChildRef();\n        this.messageActions = useMessageActions();\n        this.onClickModal = this.onClickModal.bind(this);\n        onMounted(() => {\n            this.modalRef.el.addEventListener(\"click\", this.onClickModal);\n        });\n        onWillUnmount(() => {\n            this.modalRef.el.removeEventListener(\"click\", this.onClickModal);\n        });\n    }\n\n    onClickModal() {\n        this.props.close?.();\n    }\n\n    get message() {\n        return this.props.message;\n    }\n\n    get state() {\n        return this.props.state;\n    }\n\n    async onClickAction(action) {\n        const success = await action.onClick();\n        if (action.mobileCloseAfterClick && (success || success === undefined)) {\n            this.props.close?.();\n        }\n    }\n\n    openReactionMenu() {\n        return this.props.openReactionMenu?.();\n    }\n}\n", "import { Component, toRaw, useComponent, useState, xml } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { download } from \"@web/core/network/download\";\nimport { registry } from \"@web/core/registry\";\nimport { MessageReactionButton } from \"./message_reaction_button\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { discussComponentRegistry } from \"./discuss_component_registry\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { EMOJI_PICKER_PROPS, EmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { onExternalClick } from \"@mail/utils/common/hooks\";\nimport { convertBrToLineBreak } from \"@mail/utils/common/format\";\n\nconst { DateTime } = luxon;\n\nexport const messageActionsRegistry = registry.category(\"mail.message/actions\");\n\nclass EmojiPickerMobile extends Component {\n    static components = { Dialog, EmojiPicker };\n    static props = [...EMOJI_PICKER_PROPS, \"onClose?\"];\n    static template = xml`\n        <Dialog size=\"'lg'\" header=\"false\" footer=\"false\" contentClass=\"'o-discuss-mobileContextMenu d-flex position-absolute bottom-0 rounded-0 h-50 bg-100'\">\n            <div t-ref=\"root\">\n                <EmojiPicker t-props=\"emojiPickerProps\"/>\n            </div>\n        </Dialog>\n    `;\n\n    get emojiPickerProps() {\n        return {\n            ...this.props,\n            onSelect: (...args) => {\n                this.props.onSelect(...args);\n                this.props.close?.();\n            },\n        };\n    }\n\n    setup() {\n        super.setup();\n        onExternalClick(\"root\", () => this.props.close?.());\n    }\n}\n\nmessageActionsRegistry\n    .add(\"reaction\", {\n        callComponent: MessageReactionButton,\n        props: (component) => ({\n            message: component.props.message,\n            action: messageActionsRegistry.get(\"reaction\"),\n        }),\n        condition: (component) => component.props.message.canAddReaction(component.props.thread),\n        icon: \"oi oi-smile-add\",\n        title: _t(\"Add a Reaction\"),\n        onClick: async (component) => {\n            const def = new Deferred();\n            component.dialog.add(\n                EmojiPickerMobile,\n                {\n                    onSelect: (emoji) => {\n                        const reaction = component.props.message.reactions.find(\n                            ({ content, personas }) =>\n                                content === emoji &&\n                                personas.find((persona) => persona.eq(component.store.self))\n                        );\n                        if (!reaction) {\n                            component.props.message.react(emoji);\n                        }\n                        def.resolve(true);\n                    },\n                },\n                { context: component, onClose: () => def.resolve(false) }\n            );\n            return def;\n        },\n        sequence: 10,\n    })\n    .add(\"reply-to\", {\n        condition: (component) => component.props.message.canReplyTo(component.props.thread),\n        icon: \"fa fa-reply\",\n        title: _t(\"Reply\"),\n        onClick: (component) => {\n            const message = toRaw(component.props.message);\n            const thread = toRaw(component.props.thread);\n            component.props.messageToReplyTo.toggle(thread, message);\n        },\n        sequence: (component) => (component.props.thread?.eq(component.store.inbox) ? 55 : 20),\n    })\n    .add(\"toggle-star\", {\n        condition: (component) => component.props.message.canToggleStar,\n        icon: (component) =>\n            component.props.message.starred ? \"fa fa-star o-mail-Message-starred\" : \"fa fa-star-o\",\n        title: _t(\"Mark as Todo\"),\n        onClick: (component) => component.props.message.toggleStar(),\n        sequence: 30,\n        mobileCloseAfterClick: false,\n    })\n    .add(\"mark-as-read\", {\n        condition: (component) => component.props.thread?.eq(component.store.inbox),\n        icon: \"fa fa-check\",\n        title: _t(\"Mark as Read\"),\n        onClick: (component) => component.props.message.setDone(),\n        sequence: 40,\n    })\n    .add(\"reactions\", {\n        condition: (component) => component.message.reactions.length,\n        icon: \"fa fa-smile-o\",\n        title: _t(\"View Reactions\"),\n        onClick: (component) => component.openReactionMenu(),\n        sequence: 50,\n        dropdown: true,\n    })\n    .add(\"unfollow\", {\n        condition: (component) => component.props.message.canUnfollow(component.props.thread),\n        icon: \"fa-user-times\",\n        title: _t(\"Unfollow\"),\n        onClick: (component) => component.props.message.unfollow(),\n        sequence: 60,\n    })\n    .add(\"mark-as-unread\", {\n        condition: (component) =>\n            component.props.thread?.model === \"discuss.channel\" &&\n            component.store.self.type === \"partner\",\n        icon: \"fa fa-eye-slash\",\n        title: _t(\"Mark as Unread\"),\n        onClick: (component) => component.props.message.onClickMarkAsUnread(component.props.thread),\n        sequence: 70,\n    })\n    .add(\"edit\", {\n        condition: (component) => component.props.message.editable,\n        icon: \"fa fa-pencil\",\n        title: _t(\"Edit\"),\n        onClick: (component) => {\n            const message = toRaw(component.props.message);\n            const text = convertBrToLineBreak(message.body);\n            message.composer = {\n                mentionedPartners: message.recipients,\n                text,\n                selection: {\n                    start: text.length,\n                    end: text.length,\n                    direction: \"none\",\n                },\n            };\n            component.state.isEditing = true;\n        },\n        sequence: 80,\n    })\n    .add(\"delete\", {\n        condition: (component) => component.props.message.editable,\n        icon: \"fa fa-trash\",\n        title: _t(\"Delete\"),\n        onClick: async (component) => {\n            const message = toRaw(component.message);\n            const def = new Deferred();\n            component.dialog.add(\n                discussComponentRegistry.get(\"MessageConfirmDialog\"),\n                {\n                    message,\n                    prompt: _t(\"Are you sure you want to delete this message?\"),\n                    onConfirm: () => {\n                        def.resolve(true);\n                        message.remove();\n                    },\n                },\n                { context: component, onClose: () => def.resolve(false) }\n            );\n            return def;\n        },\n        setup: () => {\n            const component = useComponent();\n            component.dialog = useService(\"dialog\");\n        },\n        sequence: 90,\n    })\n    .add(\"download_files\", {\n        condition: (component) =>\n            component.message.attachment_ids.length > 1 && component.store.self.isInternalUser,\n        icon: \"fa fa-download\",\n        title: _t(\"Download Files\"),\n        onClick: (component) =>\n            download({\n                data: {\n                    file_ids: component.message.attachment_ids.map((rec) => rec.id),\n                    zip_name: `attachments_${DateTime.local().toFormat(\"HHmmddMMyyyy\")}.zip`,\n                },\n                url: \"/mail/attachment/zip\",\n            }),\n        sequence: 55,\n    })\n    .add(\"toggle-translation\", {\n        condition: (component) => component.props.message.isTranslatable(component.props.thread),\n        icon: (component) =>\n            `fa fa-language ${component.state.showTranslation ? \"o-mail-Message-translated\" : \"\"}`,\n        title: (component) => (component.state.showTranslation ? _t(\"Revert\") : _t(\"Translate\")),\n        onClick: (component) => component.onClickToggleTranslation(),\n        sequence: 100,\n    })\n    .add(\"copy-link\", {\n        condition: (component) =>\n            component.message.message_type &&\n            component.message.message_type !== \"user_notification\",\n        icon: \"fa fa-link\",\n        title: _t(\"Copy Link\"),\n        onClick: (component) => component.message.copyLink(),\n        sequence: 110,\n    });\n\nfunction transformAction(component, id, action) {\n    return {\n        component: action.component,\n        id,\n        mobileCloseAfterClick: action.mobileCloseAfterClick ?? true,\n        /** Condition to display this action. */\n        get condition() {\n            return action.condition(component);\n        },\n        /** Icon for the button this action. */\n        get icon() {\n            return typeof action.icon === \"function\" ? action.icon(component) : action.icon;\n        },\n        /** title of this action, displayed to the user. */\n        get title() {\n            return typeof action.title === \"function\" ? action.title(component) : action.title;\n        },\n        callComponent: action.callComponent,\n        get props() {\n            return action.props(component);\n        },\n        /**\n         * Action to execute when this action is click.\n         *\n         * @param {object} [param0]\n         * @param {boolean} [param0.keepPrevious] Whether the previous action\n         * should be kept so that closing the current action goes back\n         * to the previous one.\n         * */\n        onClick() {\n            return action.onClick?.(component);\n        },\n        /** Determines the order of this action (smaller first). */\n        get sequence() {\n            return typeof action.sequence === \"function\"\n                ? action.sequence(component)\n                : action.sequence;\n        },\n        /** Component setup to execute when this action is registered. */\n        setup: action.setup,\n    };\n}\n\nexport function useMessageActions() {\n    const component = useComponent();\n    const transformedActions = messageActionsRegistry\n        .getEntries()\n        .map(([id, action]) => transformAction(component, id, action));\n    for (const action of transformedActions) {\n        if (action.setup) {\n            action.setup(action);\n        }\n    }\n    const state = useState({\n        get actions() {\n            const actions = transformedActions\n                .filter((action) => action.condition)\n                .sort((a1, a2) => a1.sequence - a2.sequence);\n            if (actions.length > 0) {\n                actions.at(0).isFirst = true;\n                actions.at(-1).isLast = true;\n            }\n            return actions;\n        },\n    });\n    return state;\n}\n", "import { Message } from \"@mail/core/common/message\";\nimport { useVisible } from \"@mail/utils/common/hooks\";\n\nimport { Component, useState, useSubEnv } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {string} [emptyText]\n * @property {import(\"@mail/core/common/message_model\").Message[]} messages\n * @property {ReturnType<import('@mail/core/common/message_search_hook').useMessageSearch>} [messageSearch]\n * @property {function} [loadMore]\n * @property {string} mode\n * @property {function} [onClickJump]\n * @property {function} [onLoadMoreVisible]\n * @property {boolean} [showEmpty]\n * @property {import(\"@mail/core/common/thread_model\").Thread} thread\n * @extends {Component<Props, Env>}\n */\nexport class MessageCardList extends Component {\n    static components = { Message };\n    static props = [\n        \"emptyText?\",\n        \"messages\",\n        \"messageSearch?\",\n        \"loadMore?\",\n        \"mode\",\n        \"onClickJump?\",\n        \"onLoadMoreVisible?\",\n        \"showEmpty?\",\n        \"thread\",\n    ];\n    static template = \"mail.MessageCardList\";\n\n    setup() {\n        super.setup();\n        this.ui = useState(useService(\"ui\"));\n        useSubEnv({ messageCard: true });\n        useVisible(\"load-more\", (isVisible) => {\n            if (isVisible) {\n                this.props.onLoadMoreVisible?.();\n            }\n        });\n    }\n\n    /**\n     * Highlight the given message and scrolls to it. In small mode, the\n     * pin/search menus are closed beforewards\n     *\n     * @param {import('@mail/core/common/message_model').Message} message\n     */\n    async onClickJump(message) {\n        this.props.onClickJump?.();\n        if (this.ui.isSmall || this.env.inChatWindow) {\n            this.env.pinMenu?.close();\n            this.env.searchMenu?.close();\n        }\n        // Give the time for menus to close before scrolling to the message.\n        await new Promise((resolve) => setTimeout(() => requestAnimationFrame(resolve)));\n        await this.env.messageHighlight?.highlightMessage(message, this.props.thread);\n    }\n\n    get emptyText() {\n        return this.props.emptyText ?? _t(\"No messages found\");\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { discussComponentRegistry } from \"./discuss_component_registry\";\n\nexport class MessageConfirmDialog extends Component {\n    static components = { Dialog };\n    static props = [\n        \"close\",\n        \"confirmColor?\",\n        \"confirmText?\",\n        \"message\",\n        \"prompt\",\n        \"size?\",\n        \"title?\",\n        \"onConfirm\",\n    ];\n    static defaultProps = {\n        confirmColor: \"btn-primary\",\n        confirmText: _t(\"Confirm\"),\n        size: \"xl\",\n        title: _t(\"Confirmation\"),\n    };\n    static template = \"mail.MessageConfirmDialog\";\n\n    get messageComponent() {\n        return discussComponentRegistry.get(\"Message\");\n    }\n\n    onClickConfirm() {\n        this.props.onConfirm();\n        this.props.close();\n    }\n}\n\ndiscussComponentRegistry.add(\"MessageConfirmDialog\", MessageConfirmDialog);\n", "import { Component, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { url } from \"@web/core/utils/urls\";\n\nexport class MessageInReply extends Component {\n    static props = [\"message\", \"onClick?\"];\n    static template = \"mail.MessageInReply\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n    }\n\n    get authorAvatarUrl() {\n        if (\n            this.props.message.message_type &&\n            this.props.message.message_type.includes(\"email\") &&\n            ![\"partner\", \"guest\"].includes(this.props.message.author?.type)\n        ) {\n            return url(\"/mail/static/src/img/email_icon.png\");\n        }\n\n        if (this.props.message.parentMessage.author) {\n            return this.props.message.parentMessage.author.avatarUrl;\n        }\n\n        return this.store.DEFAULT_AVATAR;\n    }\n}\n", "import { Record } from \"@mail/core/common/record\";\nimport {\n    EMOJI_REGEX,\n    convertBrToLineBreak,\n    htmlToTextContentInline,\n    prettifyMessageContent,\n} from \"@mail/utils/common/format\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { url } from \"@web/core/utils/urls\";\nimport { stateToUrl } from \"@web/core/browser/router\";\nimport { toRaw } from \"@odoo/owl\";\n\nconst { DateTime } = luxon;\nexport class Message extends Record {\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").Message>} */\n    static records = {};\n    /** @returns {import(\"models\").Message} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Message|import(\"models\").Message[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    /** @param {Object} data */\n    update(data) {\n        super.update(data);\n        if (this.isNotification && !this.notificationType) {\n            const parser = new DOMParser();\n            const htmlBody = parser.parseFromString(this.body, \"text/html\");\n            this.notificationType = htmlBody.querySelector(\".o_mail_notification\")?.dataset.oeType;\n        }\n    }\n\n    attachment_ids = Record.many(\"Attachment\", { inverse: \"message\" });\n    author = Record.one(\"Persona\");\n    body = Record.attr(\"\", { html: true });\n    composer = Record.one(\"Composer\", { inverse: \"message\", onDelete: (r) => r.delete() });\n    /** @type {DateTime} */\n    date = Record.attr(undefined, { type: \"datetime\" });\n    /** @type {string} */\n    default_subject;\n    /** @type {boolean} */\n    edited = Record.attr(false, {\n        compute() {\n            return Boolean(\n                new DOMParser()\n                    .parseFromString(this.body, \"text/html\")\n                    // \".o-mail-Message-edited\" is the class added by the mail.thread in _message_update_content\n                    // when the message is edited\n                    .querySelector(\".o-mail-Message-edited\")\n            );\n        },\n    });\n    hasEveryoneSeen = Record.attr(false, {\n        /** @this {import(\"models\").Message} */\n        compute() {\n            return this.thread?.membersThatCanSeen.every((m) => m.hasSeen(this));\n        },\n    });\n    isMessagePreviousToLastSelfMessageSeenByEveryone = Record.attr(false, {\n        /** @this {import(\"models\").Message} */\n        compute() {\n            if (!this.thread?.lastSelfMessageSeenByEveryone) {\n                return false;\n            }\n            return this.id < this.thread.lastSelfMessageSeenByEveryone.id;\n        },\n    });\n    isReadBySelf = Record.attr(false, {\n        compute() {\n            return (\n                this.thread?.selfMember?.seen_message_id?.id >= this.id &&\n                this.thread?.selfMember?.new_message_separator > this.id\n            );\n        },\n    });\n    hasSomeoneSeen = Record.attr(false, {\n        /** @this {import(\"models\").Message} */\n        compute() {\n            return this.thread?.membersThatCanSeen\n                .filter(({ persona }) => !persona.eq(this.author))\n                .some((m) => m.hasSeen(this));\n        },\n    });\n    hasSomeoneFetched = Record.attr(false, {\n        /** @this {import(\"models\").Message} */\n        compute() {\n            if (!this.thread) {\n                return false;\n            }\n            const otherFetched = this.thread.channelMembers.filter(\n                (m) => m.persona.notEq(this.author) && m.fetched_message_id?.id >= this.id\n            );\n            return otherFetched.length > 0;\n        },\n    });\n    hasLink = Record.attr(false, {\n        compute() {\n            if (this.isBodyEmpty) {\n                return false;\n            }\n            const div = document.createElement(\"div\");\n            div.innerHTML = this.body;\n            return Boolean(div.querySelector(\"a:not([data-oe-model])\"));\n        },\n    });\n    /** @type {number|string} */\n    id;\n    /** @type {boolean} */\n    is_discussion;\n    /** @type {boolean} */\n    is_note;\n    /** @type {boolean} */\n    is_transient;\n    linkPreviews = Record.many(\"LinkPreview\", { inverse: \"message\", onDelete: (r) => r.delete() });\n    /** @type {number[]} */\n    parentMessage = Record.one(\"Message\");\n    /**\n     * When set, this temporary/pending message failed message post, and the\n     * value is a callback to re-attempt to post the message.\n     *\n     * @type {() => {} | undefined}\n     */\n    postFailRedo = undefined;\n    reactions = Record.many(\"MessageReactions\", {\n        inverse: \"message\",\n        /**\n         * @param {import(\"models\").MessageReactions} r1\n         * @param {import(\"models\").MessageReactions} r2\n         */\n        sort: (r1, r2) => r1.sequence - r2.sequence,\n    });\n    notifications = Record.many(\"Notification\", { inverse: \"message\" });\n    recipients = Record.many(\"Persona\");\n    thread = Record.one(\"Thread\");\n    threadAsNeedaction = Record.one(\"Thread\", {\n        compute() {\n            if (this.needaction) {\n                return this.thread;\n            }\n        },\n    });\n    threadAsNewest = Record.one(\"Thread\");\n    /** @type {DateTime} */\n    scheduledDatetime = Record.attr(undefined, { type: \"datetime\" });\n    onlyEmojis = Record.attr(false, {\n        compute() {\n            const div = document.createElement(\"div\");\n            div.innerHTML = this.body;\n            const bodyWithoutTags = div.textContent;\n            const withoutEmojis = bodyWithoutTags.replace(EMOJI_REGEX, \"\");\n            return bodyWithoutTags.length > 0 && withoutEmojis.trim().length === 0;\n        },\n    });\n    /** @type {string} */\n    subject;\n    /** @type {string} */\n    subtype_description;\n    threadAsFirstUnread = Record.one(\"Thread\", { inverse: \"firstUnreadMessage\" });\n    /** @type {Object[]} */\n    trackingValues = [];\n    /** @type {string|undefined} */\n    translationValue;\n    /** @type {string|undefined} */\n    translationSource;\n    /** @type {string|undefined} */\n    translationErrors;\n    /** @type {string} */\n    message_type;\n    /** @type {string|undefined} */\n    notificationType;\n    /** @type {luxon.DateTime} */\n    create_date = Record.attr(undefined, { type: \"datetime\" });\n    /** @type {luxon.DateTime} */\n    write_date = Record.attr(undefined, { type: \"datetime\" });\n    /** @type {undefined|Boolean} */\n    needaction;\n    starred = false;\n\n    /**\n     * True if the backend would technically allow edition\n     * @returns {boolean}\n     */\n    get allowsEdition() {\n        return this.store.self.isAdmin || this.isSelfAuthored;\n    }\n\n    get bubbleColor() {\n        if (!this.isSelfAuthored && !this.is_note && !this.isHighlightedFromMention) {\n            return \"blue\";\n        }\n        if (this.isSelfAuthored && !this.is_note && !this.isHighlightedFromMention) {\n            return \"green\";\n        }\n        if (this.isHighlightedFromMention) {\n            return \"orange\";\n        }\n        return undefined;\n    }\n\n    get editable() {\n        if (!this.allowsEdition) {\n            return false;\n        }\n        return this.message_type === \"comment\";\n    }\n\n    get dateDay() {\n        let dateDay = this.datetime.toLocaleString(DateTime.DATE_FULL);\n        if (dateDay === DateTime.now().toLocaleString(DateTime.DATE_FULL)) {\n            dateDay = _t(\"Today\");\n        }\n        return dateDay;\n    }\n\n    get dateSimple() {\n        return this.datetime.toLocaleString(DateTime.TIME_24_SIMPLE, {\n            locale: user.lang,\n        });\n    }\n\n    get datetime() {\n        return this.date || DateTime.now();\n    }\n\n    get datetimeShort() {\n        return this.datetime.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS);\n    }\n\n    get isSelfMentioned() {\n        return this.store.self.in(this.recipients);\n    }\n\n    get isHighlightedFromMention() {\n        return this.isSelfMentioned && this.thread?.model === \"discuss.channel\";\n    }\n\n    isSelfAuthored = Record.attr(false, {\n        compute() {\n            if (!this.author) {\n                return false;\n            }\n            return this.author.eq(this.store.self);\n        },\n    });\n\n    isPending = false;\n\n    get hasActions() {\n        return !this.is_transient;\n    }\n\n    get isNotification() {\n        return this.message_type === \"notification\" && this.thread?.model === \"discuss.channel\";\n    }\n\n    get isSubjectSimilarToThreadName() {\n        if (!this.subject || !this.thread || !this.thread.name) {\n            return false;\n        }\n        const regexPrefix = /^((re|fw|fwd)\\s*:\\s*)*/i;\n        const cleanedThreadName = this.thread.name.replace(regexPrefix, \"\");\n        const cleanedSubject = this.subject.replace(regexPrefix, \"\");\n        return cleanedSubject === cleanedThreadName;\n    }\n\n    get isSubjectDefault() {\n        const name = this.thread?.name;\n        const threadName = name ? name.trim().toLowerCase() : \"\";\n        const defaultSubject = this.default_subject ? this.default_subject.toLowerCase() : \"\";\n        const candidates = new Set([defaultSubject, threadName]);\n        return candidates.has(this.subject?.toLowerCase());\n    }\n\n    get resUrl() {\n        return url(stateToUrl({ model: this.thread.model, resId: this.thread.id }));\n    }\n\n    isTranslatable(thread) {\n        return (\n            this.store.hasMessageTranslationFeature &&\n            ![\"discuss.channel\", \"mail.box\"].includes(thread?.model)\n        );\n    }\n\n    get hasTextContent() {\n        return !this.isBodyEmpty;\n    }\n\n    isEmpty = Record.attr(false, {\n        /** @this {import(\"models\").Message} */\n        compute() {\n            return (\n                this.isBodyEmpty &&\n                this.attachment_ids.length === 0 &&\n                this.trackingValues.length === 0 &&\n                !this.subtype_description\n            );\n        },\n    });\n    isBodyEmpty = Record.attr(undefined, {\n        compute() {\n            return (\n                !this.body ||\n                [\"\", \"<p></p>\", \"<p><br></p>\", \"<p><br/></p>\"].includes(\n                    this.body\n                        .replace('<span class=\"o-mail-Message-edited\"></span>', \"\")\n                        .replace(/\\s/g, \"\")\n                )\n            );\n        },\n    });\n\n    /**\n     * Determines if the link preview is actually the main content of the\n     * message. Meaning:\n     * - The link is the only part of the message body.\n     * - There is only one link in the message body.\n     * - The link preview is of image type.\n     */\n    get linkPreviewSquash() {\n        return (\n            this.store.hasLinkPreviewFeature &&\n            this.body &&\n            this.body.startsWith(\"<a\") &&\n            this.body.endsWith(\"/a>\") &&\n            this.body.match(/<\\/a>/im)?.length === 1 &&\n            this.linkPreviews.length === 1 &&\n            this.linkPreviews[0].isImage\n        );\n    }\n\n    get inlineBody() {\n        if (!this.body) {\n            return \"\";\n        }\n        return htmlToTextContentInline(this.body);\n    }\n\n    get notificationIcon() {\n        switch (this.notificationType) {\n            case \"pin\":\n                return \"fa fa-thumb-tack\";\n        }\n        return null;\n    }\n\n    get failureNotifications() {\n        return this.notifications.filter((notification) => notification.isFailure);\n    }\n\n    get scheduledDateSimple() {\n        return this.scheduledDatetime.toLocaleString(DateTime.TIME_24_SIMPLE, {\n            locale: user.lang,\n        });\n    }\n\n    get canToggleStar() {\n        return Boolean(\n            !this.is_transient &&\n                this.thread &&\n                this.store.self.type === \"partner\" &&\n                this.store.self.isInternalUser\n        );\n    }\n\n    /** @param {import(\"models\").Thread} thread the thread where the message is shown */\n    canAddReaction(thread) {\n        return Boolean(!this.is_transient && this.thread);\n    }\n\n    /** @param {import(\"models\").Thread} thread the thread where the message is shown */\n    canReplyTo(thread) {\n        return (\n            [\"discuss.channel\", \"mail.box\"].includes(thread.model) &&\n            this.message_type !== \"user_notification\"\n        );\n    }\n\n    /** @param {import(\"models\").Thread} thread the thread where the message is shown */\n    canUnfollow(thread) {\n        return Boolean(this.thread?.selfFollower && thread?.model === \"mail.box\");\n    }\n\n    async copyLink() {\n        let notification = _t(\"Message Link Copied!\");\n        let type = \"info\";\n        try {\n            await browser.navigator.clipboard.writeText(url(`/mail/message/${this.id}`));\n        } catch {\n            notification = _t(\"Message Link Copy Failed (Permission denied?)!\");\n            type = \"danger\";\n        }\n        this.store.env.services.notification.add(notification, { type });\n    }\n\n    async edit(body, attachments = [], { mentionedChannels = [], mentionedPartners = [] } = {}) {\n        if (convertBrToLineBreak(this.body) === body && attachments.length === 0) {\n            return;\n        }\n        const validMentions = this.store.getMentionsFromText(body, {\n            mentionedChannels,\n            mentionedPartners,\n        });\n        const data = await rpc(\"/mail/message/update_content\", {\n            attachment_ids: attachments\n                .concat(this.attachment_ids)\n                .map((attachment) => attachment.id),\n            attachment_tokens: attachments\n                .concat(this.attachment_ids)\n                .map((attachment) => attachment.access_token),\n            body: await prettifyMessageContent(body, validMentions),\n            message_id: this.id,\n            partner_ids: validMentions?.partners?.map((partner) => partner.id),\n            ...this.thread.rpcParams,\n        });\n        this.store.insert(data, { html: true });\n        if (this.hasLink && this.store.hasLinkPreviewFeature) {\n            rpc(\"/mail/link_preview\", { message_id: this.id }, { silent: true });\n        }\n    }\n\n    async react(content) {\n        this.store.insert(\n            await rpc(\n                \"/mail/message/reaction\",\n                {\n                    action: \"add\",\n                    content,\n                    message_id: this.id,\n                    ...this.thread.rpcParams,\n                },\n                { silent: true }\n            )\n        );\n    }\n\n    async remove() {\n        await rpc(\"/mail/message/update_content\", {\n            attachment_ids: [],\n            attachment_tokens: [],\n            body: \"\",\n            message_id: this.id,\n            ...this.thread.rpcParams,\n        });\n        this.body = \"\";\n        this.attachment_ids = [];\n    }\n\n    async setDone() {\n        await this.store.env.services.orm.silent.call(\"mail.message\", \"set_message_done\", [\n            [this.id],\n        ]);\n    }\n\n    async toggleStar() {\n        await this.store.env.services.orm.silent.call(\"mail.message\", \"toggle_message_starred\", [\n            [this.id],\n        ]);\n    }\n\n    async unfollow() {\n        if (this.needaction) {\n            await this.setDone();\n        }\n        const thread = this.thread;\n        await thread.selfFollower.remove();\n        this.store.env.services.notification.add(\n            _t('You are no longer following \"%(thread_name)s\".', { thread_name: thread.name }),\n            { type: \"success\" }\n        );\n    }\n\n    get channelMemberHaveSeen() {\n        return this.thread.membersThatCanSeen.filter(\n            (m) => m.hasSeen(this) && m.persona.notEq(this.author)\n        );\n    }\n\n    /** @param {import(\"models\").Thread} thread the thread where the message is shown */\n    onClickMarkAsUnread(thr) {\n        const message = toRaw(this);\n        const thread = toRaw(thr);\n        if (!thread.selfMember || thread.selfMember?.new_message_separator === message.id) {\n            return;\n        }\n        return rpc(\"/discuss/channel/mark_as_unread\", {\n            channel_id: message.thread.id,\n            message_id: message.id,\n        });\n    }\n}\n\nMessage.register();\n", "import { Component } from \"@odoo/owl\";\n\nexport class MessageNotificationPopover extends Component {\n    static template = \"mail.MessageNotificationPopover\";\n    static props = [\"message\", \"close?\"];\n}\n", "import { Component, useRef, useState } from \"@odoo/owl\";\nimport { useEmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Message} message\n * @extends {Component<Props, Env>}\n */\nexport class MessageReactionButton extends Component {\n    static template = \"mail.MessageReactionButton\";\n    static props = [\"message\", \"classNames?\", \"action\"];\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.emojiPickerRef = useRef(\"emoji-picker\");\n        this.emojiPicker = useEmojiPicker(this.emojiPickerRef, {\n            onSelect: (emoji) => {\n                const reaction = this.props.message.reactions.find(\n                    ({ content, personas }) =>\n                        content === emoji && personas.find((persona) => persona.eq(this.store.self))\n                );\n                if (!reaction) {\n                    this.props.message.react(emoji);\n                }\n            },\n        });\n    }\n}\n", "import { useHover } from \"@mail/utils/common/hooks\";\nimport { Component, onMounted, onPatched, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { loadEmoji, loader } from \"@web/core/emoji_picker/emoji_picker\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class MessageReactionList extends Component {\n    static template = \"mail.MessageReactionList\";\n    static components = { Dropdown };\n    static props = [\"message\", \"openReactionMenu\", \"reaction\"];\n\n    setup() {\n        super.setup();\n        this.loadEmoji = loadEmoji;\n        this.store = useState(useService(\"mail.store\"));\n        this.ui = useService(\"ui\");\n        this.hover = useHover([\"reactionButton\", \"reactionList*\"], {\n            onHover: () => (this.preview.isOpen = true),\n            onAway: () => (this.preview.isOpen = false),\n        });\n        this.state = useState({ emojiLoaded: Boolean(loader.loaded) });\n        if (!loader.loaded) {\n            loader.onEmojiLoaded(() => (this.state.emojiLoaded = true));\n        }\n        onMounted(() => void this.state.emojiLoaded);\n        onPatched(() => void this.state.emojiLoaded);\n        this.preview = useDropdownState();\n    }\n\n    /** @param {import(\"models\").MessageReactions} reaction */\n    previewText(reaction) {\n        const { count, content: emoji } = reaction;\n        const personNames = reaction.personas\n              .slice(0, 3)\n              .map(persona => persona.name);\n        const shortcode = loader.loaded?.emojiValueToShortcode?.[emoji] ?? \"?\";\n        switch (count) {\n            case 1:\n                return _t(\"%(emoji)s reacted by %(person)s\", {\n                    emoji: shortcode,\n                    person: personNames[0],\n                });\n            case 2:\n                return _t(\"%(emoji)s reacted by %(person1)s and %(person2)s\", {\n                    emoji: shortcode,\n                    person1: personNames[0],\n                    person2: personNames[1],\n                });\n            case 3:\n                return _t(\"%(emoji)s reacted by %(person1)s, %(person2)s, and %(person3)s\", {\n                    emoji: shortcode,\n                    person1: personNames[0],\n                    person2: personNames[1],\n                    person3: personNames[2],\n                });\n            case 4:\n                return _t(\n                    \"%(emoji)s reacted by %(person1)s, %(person2)s, %(person3)s, and 1 other\",\n                    {\n                        emoji: shortcode,\n                        person1: personNames[0],\n                        person2: personNames[1],\n                        person3: personNames[2],\n                    }\n                );\n            default:\n                return _t(\n                    \"%(emoji)s reacted by %(person1)s, %(person2)s, %(person3)s, and %(count)s others\",\n                    {\n                        count: count - 3,\n                        emoji: shortcode,\n                        person1: personNames[0],\n                        person2: personNames[1],\n                        person3: personNames[2],\n                    }\n                );\n        }\n    }\n\n    hasSelfReacted(reaction) {\n        return this.store.self.in(reaction.personas);\n    }\n\n    onClickReaction(reaction) {\n        if (this.hasSelfReacted(reaction)) {\n            reaction.remove();\n        } else {\n            this.props.message.react(reaction.content);\n        }\n    }\n\n    onContextMenu(ev) {\n        if (this.ui.isSmall) {\n            ev.preventDefault();\n            this.props.openReactionMenu();\n        }\n    }\n}\n", "import { loadEmoji, loader } from \"@web/core/emoji_picker/emoji_picker\";\nimport { onExternalClick } from \"@mail/utils/common/hooks\";\n\nimport {\n    Component,\n    onMounted,\n    onPatched,\n    useEffect,\n    useExternalListener,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class MessageReactionMenu extends Component {\n    static props = [\"close\", \"message\", \"initialReaction?\"];\n    static components = { Dialog };\n    static template = \"mail.MessageReactionMenu\";\n\n    setup() {\n        super.setup();\n        this.root = useRef(\"root\");\n        this.store = useState(useService(\"mail.store\"));\n        this.ui = useState(useService(\"ui\"));\n        this.state = useState({\n            emojiLoaded: Boolean(loader.loaded),\n            reaction: this.props.initialReaction\n                ? this.props.initialReaction\n                : this.props.message.reactions[0],\n        });\n        useExternalListener(document, \"keydown\", this.onKeydown);\n        onExternalClick(\"root\", () => this.props.close());\n        useEffect(\n            () => {\n                const activeReaction = this.props.message.reactions.find(\n                    ({ content }) => content === this.state.reaction.content\n                );\n                if (this.props.message.reactions.length === 0) {\n                    this.props.close();\n                } else if (!activeReaction) {\n                    this.state.reaction = this.props.message.reactions[0];\n                }\n            },\n            () => [this.props.message.reactions.length]\n        );\n        onMounted(async () => {\n            if (!loader.loaded) {\n                loadEmoji();\n            }\n        });\n        if (!loader.loaded) {\n            loader.onEmojiLoaded(() => (this.state.emojiLoaded = true));\n        }\n        onMounted(() => void this.state.emojiLoaded);\n        onPatched(() => void this.state.emojiLoaded);\n    }\n\n    onKeydown(ev) {\n        switch (ev.key) {\n            case \"Escape\":\n                this.props.close();\n                break;\n            case \"q\":\n                this.props.close();\n                break;\n            default:\n                return;\n        }\n    }\n\n    getEmojiShortcode(reaction) {\n        return loader.loaded?.emojiValueToShortcode?.[reaction.content] ?? \"?\";\n    }\n}\n", "import { Component, useRef, useState } from \"@odoo/owl\";\n\nimport { MessageReactionList } from \"@mail/core/common/message_reaction_list\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useEmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\n\nexport class MessageReactions extends Component {\n    static props = [\"message\", \"openReactionMenu\"];\n    static template = \"mail.MessageReactions\";\n    static components = { MessageReactionList };\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.ui = useService(\"ui\");\n        this.addRef = useRef(\"add\");\n        this.emojiPicker = useEmojiPicker(this.addRef, {\n            onSelect: (emoji) => {\n                const reaction = this.props.message.reactions.find(\n                    ({ content, personas }) =>\n                        content === emoji && personas.find((persona) => persona.eq(this.store.self))\n                );\n                if (!reaction) {\n                    this.props.message.react(emoji);\n                }\n            },\n        });\n    }\n}\n", "import { AND, Record } from \"@mail/core/common/record\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport class MessageReactions extends Record {\n    static id = AND(\"message\", \"content\");\n    /** @returns {import(\"models\").MessageReactions} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").MessageReactions|import(\"models\").MessageReactions[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    /** @type {string} */\n    content;\n    /** @type {number} */\n    count;\n    /** @type {number} */\n    sequence;\n    personas = Record.many(\"Persona\");\n    message = Record.one(\"Message\");\n\n    async remove() {\n        this.store.insert(\n            await rpc(\n                \"/mail/message/reaction\",\n                {\n                    action: \"remove\",\n                    content: this.content,\n                    message_id: this.message.id,\n                    ...this.message.thread.rpcParams,\n                },\n                { silent: true }\n            )\n        );\n    }\n}\n\nMessageReactions.register();\n", "import { useSequential } from \"@mail/utils/common/hooks\";\nimport { useState, onWillUnmount, markup } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\n\nexport const HIGHLIGHT_CLASS = \"o-mail-Message-searchHighlight\";\n\n/**\n * @param {string} searchTerm\n * @param {string} target\n */\nexport function searchHighlight(searchTerm, target) {\n    if (!searchTerm) {\n        return target;\n    }\n    const htmlDoc = new DOMParser().parseFromString(target, \"text/html\");\n    for (const term of searchTerm.split(\" \")) {\n        const regexp = new RegExp(`(${escapeRegExp(term)})`, \"gi\");\n        // Special handling for '\n        // Note: browsers use XPath 1.0, so uses concat() rather than ||\n        const split = term.toLowerCase().split(\"'\");\n        let lowercase = split.map((s) => `'${s}'`).join(', \"\\'\", ');\n        let uppercase = lowercase.toUpperCase();\n        if (split.length > 1) {\n            lowercase = `concat(${lowercase})`;\n            uppercase = `concat(${uppercase})`;\n        }\n        const matchs = htmlDoc.evaluate(\n            `//*[text()[contains(translate(., ${uppercase}, ${lowercase}), ${lowercase})]]`, // Equivalent to `.toLowerCase()` on all searched chars\n            htmlDoc,\n            null,\n            XPathResult.ORDERED_NODE_SNAPSHOT_TYPE\n        );\n        for (let i = 0; i < matchs.snapshotLength; i++) {\n            const element = matchs.snapshotItem(i);\n            const newNode = [];\n            for (const node of element.childNodes) {\n                const match = node.textContent.match(regexp);\n                if (node.nodeType === Node.TEXT_NODE && match?.length > 0) {\n                    let curIndex = 0;\n                    for (const match of node.textContent.matchAll(regexp)) {\n                        const start = htmlDoc.createTextNode(\n                            node.textContent.slice(curIndex, match.index)\n                        );\n                        newNode.push(start);\n                        const span = htmlDoc.createElement(\"span\");\n                        span.setAttribute(\"class\", HIGHLIGHT_CLASS);\n                        span.textContent = match[0];\n                        newNode.push(span);\n                        curIndex = match.index + match[0].length;\n                    }\n                    const end = htmlDoc.createTextNode(node.textContent.slice(curIndex));\n                    newNode.push(end);\n                } else {\n                    newNode.push(node);\n                }\n            }\n            element.replaceChildren(...newNode);\n        }\n    }\n    return markup(htmlDoc.body.innerHTML);\n}\n\n/** @param {import('models').Thread} thread */\nexport function useMessageSearch(thread) {\n    const store = useService(\"mail.store\");\n    const sequential = useSequential();\n    const state = useState({\n        thread,\n        async search(before = false) {\n            if (this.searchTerm) {\n                this.searching = true;\n                const data = await sequential(() =>\n                    store.search(this.searchTerm, this.thread, before)\n                );\n                if (!data) {\n                    return;\n                }\n                const { count, loadMore, messages } = data;\n                this.searched = true;\n                this.searching = false;\n                this.count = count;\n                this.loadMore = loadMore;\n                if (before) {\n                    this.messages.push(...messages);\n                } else {\n                    this.messages = messages;\n                }\n            } else {\n                this.clear();\n            }\n        },\n        count: 0,\n        clear() {\n            this.messages = [];\n            this.searched = false;\n            this.searching = false;\n            this.searchTerm = undefined;\n        },\n        loadMore: false,\n        /** @type {import('@mail/core/common/message_model').Message[]} */\n        messages: [],\n        /** @type {string|undefined} */\n        searchTerm: undefined,\n        searched: false,\n        searching: false,\n        /** @param {string} target */\n        highlight: (target) => searchHighlight(state.searchTerm, target),\n    });\n    onWillUnmount(() => {\n        state.clear();\n    });\n    return state;\n}\n", "import { Component, useExternalListener, useRef } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { browser } from \"@web/core/browser/browser\";\n\nclass MessageSeenIndicatorDialog extends Component {\n    static components = { Dialog };\n    static template = \"mail.MessageSeenIndicatorDialog\";\n    static props = [\"message\", \"close?\"];\n\n    setup() {\n        super.setup();\n        this.contentRef = useRef(\"content\");\n        useExternalListener(\n            browser,\n            \"click\",\n            (ev) => {\n                if (!this.contentRef?.el.contains(ev.target)) {\n                    this.props.close();\n                }\n            },\n            true\n        );\n    }\n}\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Message} message\n * @property {import(\"models\").Thread} thread\n * @extends {Component<Props, Env>}\n */\nexport class MessageSeenIndicator extends Component {\n    static template = \"mail.MessageSeenIndicator\";\n    static props = [\"message\", \"thread\", \"className?\"];\n\n    setup() {\n        super.setup();\n        this.dialog = useService(\"dialog\");\n    }\n\n    get summary() {\n        if (this.props.message.hasEveryoneSeen) {\n            if (this.props.thread.channelMembers.length === 2) {\n                return _t(\"Seen by %(user)s\", { user: this.props.thread.correspondent.name });\n            }\n            return _t(\"Seen by everyone\");\n        }\n        const seenMembers = this.props.message.channelMemberHaveSeen;\n        const [user1, user2, user3] = seenMembers.map((member) => member.name);\n        switch (seenMembers.length) {\n            case 0:\n                return _t(\"Sent\");\n            case 1:\n                return _t(\"Seen by %(user)s\", { user: user1 });\n            case 2:\n                return _t(\"Seen by %(user1)s and %(user2)s\", { user1, user2 });\n            case 3:\n                return _t(\"Seen by %(user1)s, %(user2)s and %(user3)s\", { user1, user2, user3 });\n            case 4:\n                return _t(\"Seen by %(user1)s, %(user2)s, %(user3)s and 1 other\", {\n                    user1,\n                    user2,\n                    user3,\n                });\n            default:\n                return _t(\"Seen by %(user1)s, %(user2)s, %(user3)s and %(count)s others\", {\n                    user1,\n                    user2,\n                    user3,\n                    count: seenMembers.length - 3,\n                });\n        }\n    }\n\n    openDialog() {\n        if (this.props.message.channelMemberHaveSeen.length === 0) {\n            return;\n        }\n        this.dialog.add(MessageSeenIndicatorDialog, { message: this.props.message });\n    }\n}\n", "import { ImStatus } from \"@mail/core/common/im_status\";\nimport { onExternalClick } from \"@mail/utils/common/hooks\";\nimport { markEventHandled, isEventHandled } from \"@web/core/utils/misc\";\n\nimport { Component, useEffect, useExternalListener, useRef, useState } from \"@odoo/owl\";\n\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { usePosition } from \"@web/core/position/position_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class NavigableList extends Component {\n    static components = { ImStatus };\n    static template = \"mail.NavigableList\";\n    static props = {\n        anchorRef: { optional: true },\n        autoSelectFirst: { type: Boolean, optional: true },\n        class: { type: String, optional: true },\n        hint: { type: String, optional: true },\n        onSelect: { type: Function },\n        options: { type: Array },\n        optionTemplate: { type: String, optional: true },\n        position: { type: String, optional: true },\n        isLoading: { type: Boolean, optional: true },\n    };\n    static defaultProps = { position: \"bottom\", isLoading: false, autoSelectFirst: true };\n\n    setup() {\n        super.setup();\n        this.rootRef = useRef(\"root\");\n        this.state = useState({\n            activeIndex: null,\n            open: false,\n            showLoading: false,\n        });\n        this.hotkey = useService(\"hotkey\");\n        this.hotkeysToRemove = [];\n\n        useExternalListener(window, \"keydown\", this.onKeydown, true);\n        onExternalClick(\"root\", async (ev) => {\n            // Let event be handled by bubbling handlers first.\n            await new Promise(setTimeout);\n            if (\n                isEventHandled(ev, \"composer.onClickTextarea\") ||\n                isEventHandled(ev, \"channelSelector.onClickInput\")\n            ) {\n                return;\n            }\n            this.close();\n        });\n        // position and size\n        usePosition(\"root\", () => this.props.anchorRef, { position: this.props.position });\n        useEffect(\n            () => {\n                this.open();\n            },\n            () => [this.props]\n        );\n        useEffect(\n            () => {\n                if (!this.props.isLoading) {\n                    clearTimeout(this.loadingTimeoutId);\n                    this.state.showLoading = false;\n                } else if (!this.loadingTimeoutId) {\n                    this.loadingTimeoutId = setTimeout(() => (this.state.showLoading = true), 2000);\n                }\n            },\n            () => [this.props.isLoading]\n        );\n    }\n\n    get show() {\n        return Boolean(this.state.open && (this.props.isLoading || this.props.options.length));\n    }\n\n    get sortedOptions() {\n        return this.props.options.sort((o1, o2) => (o1.group ?? 0) - (o2.group ?? 0));\n    }\n\n    open() {\n        this.state.open = true;\n        this.state.activeIndex = null;\n        if (this.props.autoSelectFirst) {\n            this.navigate(\"first\");\n        }\n    }\n\n    close() {\n        this.state.open = false;\n        this.state.activeIndex = null;\n    }\n\n    selectOption(ev, index, params = {}) {\n        const option = this.props.options[index];\n        if (option.unselectable) {\n            this.close();\n            return;\n        }\n        this.props.onSelect(ev, option, {\n            ...params,\n        });\n        this.close();\n    }\n\n    navigate(direction) {\n        if (this.props.options.length === 0) {\n            return;\n        }\n        const activeOptionId = this.state.activeIndex !== null ? this.state.activeIndex : 0;\n        let targetId = undefined;\n        switch (direction) {\n            case \"first\":\n                targetId = 0;\n                break;\n            case \"last\":\n                targetId = this.props.options.length - 1;\n                break;\n            case \"previous\":\n                targetId = activeOptionId - 1;\n                if (targetId < 0) {\n                    this.navigate(\"last\");\n                    return;\n                }\n                break;\n            case \"next\":\n                targetId = activeOptionId + 1;\n                if (targetId > this.props.options.length - 1) {\n                    this.navigate(\"first\");\n                    return;\n                }\n                break;\n            default:\n                return;\n        }\n        this.state.activeIndex = targetId;\n    }\n\n    onKeydown(ev) {\n        if (!this.show) {\n            return;\n        }\n        const hotkey = getActiveHotkey(ev);\n        switch (hotkey) {\n            case \"enter\":\n                markEventHandled(ev, \"NavigableList.select\");\n                if (this.state.activeIndex === null) {\n                    this.close();\n                    return;\n                }\n                this.selectOption(ev, this.state.activeIndex);\n                break;\n            case \"escape\":\n                markEventHandled(ev, \"NavigableList.close\");\n                this.close();\n                break;\n            case \"tab\":\n                this.navigate(this.state.activeIndex === null ? \"first\" : \"next\");\n                break;\n            case \"arrowup\":\n                this.navigate(this.state.activeIndex === null ? \"first\" : \"previous\");\n                break;\n            case \"arrowdown\":\n                this.navigate(this.state.activeIndex === null ? \"first\" : \"next\");\n                break;\n            default:\n                return;\n        }\n        if (this.props.options.length !== 0) {\n            ev.stopPropagation();\n        }\n        ev.preventDefault();\n    }\n\n    onOptionMouseEnter(index) {\n        this.state.activeIndex = index;\n    }\n}\n", "import { Record } from \"@mail/core/common/record\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class Notification extends Record {\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").Notification>} */\n    static records = {};\n    /** @returns {import(\"models\").Notification} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Notification|import(\"models\").Notification[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    /** @type {number} */\n    id;\n    message = Record.one(\"Message\");\n    /** @type {string} */\n    notification_status;\n    /** @type {string} */\n    notification_type;\n    failure = Record.one(\"Failure\", {\n        inverse: \"notifications\",\n        /** @this {import(\"models\").Notification} */\n        compute() {\n            const thread = this.message?.thread;\n            if (!this.message?.isSelfAuthored) {\n                return;\n            }\n            const failure = Object.values(this.store.Failure.records).find((f) => {\n                return (\n                    f.resModel === thread?.model &&\n                    f.type === this.notification_type &&\n                    (f.resModel !== \"discuss.channel\" || f.resIds.has(thread?.id))\n                );\n            });\n            return this.isFailure\n                ? {\n                      id: failure ? failure.id : this.store.Failure.nextId.value++,\n                  }\n                : false;\n        },\n        eager: true,\n    });\n    /** @type {string} */\n    failure_type;\n    persona = Record.one(\"Persona\");\n\n    get isFailure() {\n        return [\"exception\", \"bounce\"].includes(this.notification_status);\n    }\n\n    get icon() {\n        if (this.isFailure) {\n            return \"fa fa-envelope\";\n        }\n        return \"fa fa-envelope-o\";\n    }\n\n    get label() {\n        return \"\";\n    }\n\n    get statusIcon() {\n        switch (this.notification_status) {\n            case \"process\":\n                return \"fa fa-hourglass-half\";\n            case \"pending\":\n                return \"fa fa-paper-plane-o\";\n            case \"sent\":\n                return \"fa fa-check\";\n            case \"bounce\":\n                return \"fa fa-exclamation\";\n            case \"exception\":\n                return \"fa fa-exclamation\";\n            case \"ready\":\n                return \"fa fa-send-o\";\n            case \"canceled\":\n                return \"fa fa-trash-o\";\n        }\n        return \"\";\n    }\n\n    get statusTitle() {\n        switch (this.notification_status) {\n            case \"process\":\n                return _t(\"Processing\");\n            case \"pending\":\n                return _t(\"Sent\");\n            case \"sent\":\n                return _t(\"Delivered\");\n            case \"bounce\":\n                return _t(\"Bounced\");\n            case \"exception\":\n                return _t(\"Error\");\n            case \"ready\":\n                return _t(\"Ready\");\n            case \"canceled\":\n                return _t(\"Cancelled\");\n        }\n        return \"\";\n    }\n}\n\nNotification.register();\n", "import { reactive } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { isAndroidApp, isIosApp } from \"@web/core/browser/feature_detection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nexport const notificationPermissionService = {\n    dependencies: [\"notification\"],\n\n    _normalizePermission(permission) {\n        switch (permission) {\n            case \"default\":\n                return \"prompt\";\n            case undefined:\n                return \"denied\";\n            default:\n                return permission;\n        }\n    },\n\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    async start(env, services) {\n        const notification = services.notification;\n        let permission;\n        try {\n            permission = await browser.navigator?.permissions?.query({\n                name: \"notifications\",\n            });\n        } catch {\n            // noop\n        }\n        const state = reactive({\n            /** @type {\"prompt\" | \"granted\" | \"denied\"} */\n            permission:\n                isIosApp() || isAndroidApp()\n                    ? \"denied\"\n                    : this._normalizePermission(\n                          permission?.state ?? browser.Notification?.permission\n                      ),\n            requestPermission: async () => {\n                if (browser.Notification && state.permission === \"prompt\") {\n                    state.permission = this._normalizePermission(\n                        await browser.Notification.requestPermission()\n                    );\n                    if (state.permission === \"denied\") {\n                        notification.add(_t(\"Odoo will not send notifications on this device.\"), {\n                            type: \"warning\",\n                            title: _t(\"Notifications blocked\"),\n                        });\n                    } else if (state.permission === \"granted\") {\n                        notification.add(_t(\"Odoo will send notifications on this device!\"), {\n                            type: \"success\",\n                            title: _t(\"Notifications allowed\"),\n                        });\n                    }\n                }\n            },\n        });\n        if (permission) {\n            permission.addEventListener(\"change\", () => (state.permission = permission.state));\n        }\n        return state;\n    },\n};\n\nregistry.category(\"services\").add(\"mail.notification.permission\", notificationPermissionService);\n", "import { htmlToTextContentInline } from \"@mail/utils/common/format\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { url } from \"@web/core/utils/urls\";\n\nconst PREVIEW_MSG_MAX_SIZE = 350; // optimal for native English speakers\n\n/**\n * @typedef {Messaging} Messaging\n */\nexport class OutOfFocusService {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    constructor(env, services) {\n        this.setup(env, services);\n    }\n\n    setup(env, services) {\n        this.env = env;\n        this.audio = undefined;\n        this.multiTab = services.multi_tab;\n        this.notificationService = services.notification;\n        this.closeFuncs = [];\n    }\n\n    async notify(message, thread) {\n        const modelsHandleByPush = [\"mail.thread\", \"discuss.channel\"];\n        if (\n            modelsHandleByPush.includes(message.thread?.model) &&\n            (await this.hasServiceWorkInstalledAndPushSubscriptionActive())\n        ) {\n            return;\n        }\n        const author = message.author;\n        let notificationTitle;\n        if (!author) {\n            notificationTitle = _t(\"New message\");\n        } else {\n            if (message.thread?.channel_type === \"channel\") {\n                notificationTitle = _t(\"%(author name)s from %(channel name)s\", {\n                    \"author name\": author.name,\n                    \"channel name\": message.thread.displayName,\n                });\n            } else {\n                notificationTitle = author.name;\n            }\n        }\n        const notificationContent = htmlToTextContentInline(message.body).substring(\n            0,\n            PREVIEW_MSG_MAX_SIZE\n        );\n        this.sendNotification({\n            message: notificationContent,\n            sound: message.thread?.model === \"discuss.channel\",\n            title: notificationTitle,\n            type: \"info\",\n        });\n    }\n\n    async hasServiceWorkInstalledAndPushSubscriptionActive() {\n        const registration = await browser.navigator.serviceWorker?.getRegistration();\n        if (registration) {\n            const pushManager = await registration.pushManager;\n            if (pushManager) {\n                const subscription = await pushManager.getSubscription();\n                return !!subscription;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Send a notification, preferably a native one. If native\n     * notifications are disable or unavailable on the current\n     * platform, fallback on the notification service.\n     *\n     * @param {Object} param0\n     * @param {string} [param0.message] The body of the\n     * notification.\n     * @param {string} [param0.title] The title of the notification.\n     * @param {string} [param0.type] The type to be passed to the no\n     * service when native notifications can't be sent.\n     */\n    sendNotification({ message, sound = true, title, type }) {\n        if (!this.canSendNativeNotification) {\n            this.sendOdooNotification(message, { sound, title, type });\n            return;\n        }\n        if (!this.multiTab.isOnMainTab()) {\n            return;\n        }\n        try {\n            this.sendNativeNotification(title, message, { sound });\n        } catch (error) {\n            // Notification without Serviceworker in Chrome Android doesn't works anymore\n            // So we fallback to the notification service in this case\n            // https://bugs.chromium.org/p/chromium/issues/detail?id=481856\n            if (error.message.includes(\"ServiceWorkerRegistration\")) {\n                this.sendOdooNotification(message, { sound, title, type });\n            } else {\n                throw error;\n            }\n        }\n    }\n\n    /**\n     * @param {string} message\n     * @param {Object} options\n     */\n    async sendOdooNotification(message, options) {\n        const { sound } = options;\n        delete options.sound;\n        this.closeFuncs.push(this.notificationService.add(message, options));\n        if (this.closeFuncs.length > 3) {\n            this.closeFuncs.shift()();\n        }\n        if (sound) {\n            this._playSound();\n        }\n    }\n\n    /**\n     * @param {string} title\n     * @param {string} message\n     */\n    sendNativeNotification(title, message, { sound = true } = {}) {\n        const notification = new Notification(title, {\n            body: message,\n            icon: \"/mail/static/src/img/odoobot_transparent.png\",\n        });\n        notification.addEventListener(\"click\", ({ target: notification }) => {\n            window.focus();\n            notification.close();\n        });\n        if (sound) {\n            this._playSound();\n        }\n    }\n\n    async _playSound() {\n        if (this.canPlayAudio && this.multiTab.isOnMainTab()) {\n            if (!this.audio) {\n                this.audio = new Audio();\n                this.audio.src = this.audio.canPlayType(\"audio/ogg; codecs=vorbis\")\n                    ? url(\"/mail/static/src/audio/ting.ogg\")\n                    : url(\"/mail/static/src/audio/ting.mp3\");\n            }\n            try {\n                await this.audio.play();\n            } catch {\n                // Ignore errors due to the user not having interracted\n                // with the page before playing the sound.\n            }\n        }\n    }\n\n    get canPlayAudio() {\n        return typeof Audio !== \"undefined\";\n    }\n\n    get canSendNativeNotification() {\n        return Boolean(browser.Notification && browser.Notification.permission === \"granted\");\n    }\n}\n\nexport const outOfFocusService = {\n    dependencies: [\"multi_tab\", \"notification\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    start(env, services) {\n        const service = new OutOfFocusService(env, services);\n        return service;\n    },\n};\n\nregistry.category(\"services\").add(\"mail.out_of_focus\", outOfFocusService);\n", "import { cleanTerm } from \"@mail/utils/common/format\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * Registry of functions to sort partner suggestions.\n * The expected value is a function with the following\n * signature:\n *     (partner1: Partner, partner2: Partner, { env: OdooEnv, searchTerm: string, thread?: Thread , context?: Object}) => number|undefined\n */\nexport const partnerCompareRegistry = registry.category(\"mail.partner_compare\");\n\npartnerCompareRegistry.add(\n    \"mail.archived-last-except-odoobot\",\n    (p1, p2) => {\n        const p1active = p1.active || p1.eq(p1.store.odoobot);\n        const p2active = p2.active || p2.eq(p2.store.odoobot);\n        if (!p1active && p2active) {\n            return 1;\n        }\n        if (!p2active && p1active) {\n            return -1;\n        }\n    },\n    { sequence: 5 }\n);\n\npartnerCompareRegistry.add(\n    \"mail.internal-users\",\n    (p1, p2) => {\n        const isAInternalUser = p1.isInternalUser;\n        const isBInternalUser = p2.isInternalUser;\n        if (isAInternalUser && !isBInternalUser) {\n            return -1;\n        }\n        if (!isAInternalUser && isBInternalUser) {\n            return 1;\n        }\n    },\n    { sequence: 35 }\n);\n\npartnerCompareRegistry.add(\n    \"mail.followers\",\n    (p1, p2, { thread }) => {\n        if (thread) {\n            const followerList = [...thread.followers];\n            if (thread.selfFollower) {\n                followerList.push(thread.selfFollower);\n            }\n            const isFollower1 = followerList.some((follower) => p1.eq(follower.partner));\n            const isFollower2 = followerList.some((follower) => p2.eq(follower.partner));\n            if (isFollower1 && !isFollower2) {\n                return -1;\n            }\n            if (!isFollower1 && isFollower2) {\n                return 1;\n            }\n        }\n    },\n    { sequence: 45 }\n);\n\npartnerCompareRegistry.add(\n    \"mail.name\",\n    (p1, p2, { searchTerm }) => {\n        const cleanedName1 = cleanTerm(p1.name);\n        const cleanedName2 = cleanTerm(p2.name);\n        if (cleanedName1.startsWith(searchTerm) && !cleanedName2.startsWith(searchTerm)) {\n            return -1;\n        }\n        if (!cleanedName1.startsWith(searchTerm) && cleanedName2.startsWith(searchTerm)) {\n            return 1;\n        }\n        if (cleanedName1 < cleanedName2) {\n            return -1;\n        }\n        if (cleanedName1 > cleanedName2) {\n            return 1;\n        }\n    },\n    { sequence: 50 }\n);\n\npartnerCompareRegistry.add(\n    \"mail.email\",\n    (p1, p2, { searchTerm }) => {\n        const cleanedEmail1 = cleanTerm(p1.email);\n        const cleanedEmail2 = cleanTerm(p2.email);\n        if (cleanedEmail1.startsWith(searchTerm) && !cleanedEmail1.startsWith(searchTerm)) {\n            return -1;\n        }\n        if (!cleanedEmail2.startsWith(searchTerm) && cleanedEmail2.startsWith(searchTerm)) {\n            return 1;\n        }\n        if (cleanedEmail1 < cleanedEmail2) {\n            return -1;\n        }\n        if (cleanedEmail1 > cleanedEmail2) {\n            return 1;\n        }\n    },\n    { sequence: 55 }\n);\n\npartnerCompareRegistry.add(\n    \"mail.id\",\n    (p1, p2) => {\n        return p1.id - p2.id;\n    },\n    { sequence: 75 }\n);\n", "import { AND, Record } from \"@mail/core/common/record\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { rpc } from \"@web/core/network/rpc\";\n\n/**\n * @typedef {'offline' | 'bot' | 'online' | 'away' | 'im_partner' | undefined} ImStatus\n * @typedef Data\n * @property {number} id\n * @property {string} name\n * @property {string} email\n * @property {'partner'|'guest'} type\n * @property {ImStatus} im_status\n */\n\nexport class Persona extends Record {\n    static id = AND(\"type\", \"id\");\n    /** @type {Object.<number, import(\"models\").Persona>} */\n    static records = {};\n    /** @returns {import(\"models\").Persona} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").Persona|import(\"models\").Persona[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    channelMembers = Record.many(\"ChannelMember\");\n    /** @type {number} */\n    id;\n    /** @type {boolean | undefined} */\n    is_company;\n    /** @type {string} */\n    landlineNumber;\n    /** @type {string} */\n    mobileNumber;\n    storeAsTrackedImStatus = Record.one(\"Store\", {\n        /** @this {import(\"models\").Persona} */\n        compute() {\n            if (\n                this.type === \"guest\" ||\n                (this.type === \"partner\" && this.im_status !== \"im_partner\" && !this.is_public)\n            ) {\n                return this.store;\n            }\n        },\n        onAdd() {\n            if (!this.store.env.services.bus_service.isActive) {\n                return;\n            }\n            const model = this.type === \"partner\" ? \"res.partner\" : \"mail.guest\";\n            this.store.env.services.bus_service.addChannel(`odoo-presence-${model}_${this.id}`);\n        },\n        onDelete() {\n            if (!this.store.env.services.bus_service.isActive) {\n                return;\n            }\n            const model = this.type === \"partner\" ? \"res.partner\" : \"mail.guest\";\n            this.store.env.services.bus_service.deleteChannel(`odoo-presence-${model}_${this.id}`);\n        },\n        eager: true,\n        inverse: \"imStatusTrackedPersonas\",\n    });\n    /** @type {'partner' | 'guest'} */\n    type;\n    /** @type {string} */\n    name;\n    country = Record.one(\"Country\");\n    /** @type {string} */\n    email;\n    /** @type {number} */\n    userId;\n    /** @type {ImStatus} */\n    im_status;\n    /** @type {'email' | 'inbox'} */\n    notification_preference;\n    isAdmin = false;\n    isInternalUser = false;\n    /** @type {luxon.DateTime} */\n    write_date = Record.attr(undefined, { type: \"datetime\" });\n\n    /**\n     * @returns {boolean}\n     */\n    get hasPhoneNumber() {\n        return Boolean(this.mobileNumber || this.landlineNumber);\n    }\n\n    get emailWithoutDomain() {\n        return this.email.substring(0, this.email.lastIndexOf(\"@\"));\n    }\n\n    get avatarUrl() {\n        if (this.type === \"partner\") {\n            return imageUrl(\"res.partner\", this.id, \"avatar_128\", { unique: this.write_date });\n        }\n        if (this.type === \"guest\") {\n            return imageUrl(\"mail.guest\", this.id, \"avatar_128\", { unique: this.write_date });\n        }\n        if (this.userId) {\n            return imageUrl(\"res.users\", this.userId, \"avatar_128\", { unique: this.write_date });\n        }\n        return this.store.DEFAULT_AVATAR;\n    }\n\n    searchChat() {\n        return Object.values(this.store.Thread.records).find(\n            (thread) => thread.channel_type === \"chat\" && thread.correspondent?.persona.eq(this)\n        );\n    }\n\n    async updateGuestName(name) {\n        await rpc(\"/mail/guest/update_name\", {\n            guest_id: this.id,\n            name,\n        });\n    }\n}\n\nPersona.register();\n", "import { Component, useExternalListener, useState } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { isEventHandled } from \"@web/core/utils/misc\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { PickerContent } from \"@mail/core/common/picker_content\";\nimport { useLazyExternalListener } from \"@mail/utils/common/hooks\";\n\nexport function usePicker(setting) {\n    const storeScroll = {\n        scrollValue: 0,\n        set: (value) => (storeScroll.scrollValue = value),\n        get: () => storeScroll.scrollValue,\n    };\n    const PICKERS = {\n        NONE: \"none\",\n        EMOJI: \"emoji\",\n        GIF: \"gif\",\n    };\n    return useState({\n        PICKERS,\n        anchor: setting.anchor,\n        buttons: setting.buttons,\n        close: setting.close,\n        pickers: setting.pickers,\n        position: setting.position,\n        state: {\n            picker: PICKERS.NONE,\n            searchTerm: \"\",\n        },\n        storeScroll,\n    });\n}\n\n/**\n * Picker/usePicker is a component hook that can be used to display the emoji picker/gif picker (if it is enabled).\n * It can be used in two ways:\n * - with a popover when in large screen: the picker will be displayed in a popover triggered by provided buttons.\n * - with a keyboard when in mobile view: the picker will be displayed in place where the Picker component is placed.\n * The switch between the two modes is done automatically based on the screen size.\n */\n\nexport class Picker extends Component {\n    static components = {\n        PickerContent,\n    };\n    static props = [\n        \"PICKERS\",\n        \"anchor?\",\n        \"buttons\",\n        \"close?\",\n        \"state\",\n        \"pickers\",\n        \"position?\",\n        \"storeScroll\",\n        \"fixed?\",\n    ];\n    static template = \"mail.Picker\";\n\n    setup() {\n        this.ui = useState(useService(\"ui\"));\n        this.popover = usePopover(PickerContent, this.popoverSettings);\n        useExternalListener(\n            browser,\n            \"click\",\n            async (ev) => {\n                if (this.props.state.picker === this.props.PICKERS.NONE) {\n                    return;\n                }\n                await new Promise(setTimeout); // let bubbling to catch marked event handled\n                if (!this.isEventHandledByPicker(ev)) {\n                    this.close();\n                }\n            },\n            true\n        );\n        for (const button of this.props.buttons) {\n            useLazyExternalListener(\n                () => button.el,\n                \"click\",\n                async (ev) => this.toggle(this.props.anchor?.el ?? button.el, ev)\n            );\n        }\n    }\n\n    get popoverSettings() {\n        return {\n            position: this.props.position,\n            fixedPosition: this.props.fixed,\n            onClose: () => this.close(),\n            closeOnClickAway: false,\n            animation: false,\n            arrow: false,\n        };\n    }\n\n    get contentProps() {\n        const pickers = {};\n        for (const [name, fn] of Object.entries(this.props.pickers)) {\n            pickers[name] = (str, resetOnSelect) => {\n                fn(str);\n                if (resetOnSelect) {\n                    this.close();\n                }\n            };\n        }\n        return {\n            PICKERS: this.props.PICKERS,\n            close: () => this.close(),\n            pickers,\n            state: this.props.state,\n            storeScroll: this.props.storeScroll,\n        };\n    }\n\n    /**\n     * @param {Event} ev\n     * @returns {boolean}\n     */\n    isEventHandledByPicker(ev) {\n        return (\n            isEventHandled(ev, \"Composer.onClickAddEmoji\") ||\n            isEventHandled(ev, \"PickerContent.onClick\")\n        );\n    }\n\n    async toggle(el, ev) {\n        // Let event be handled by bubbling handlers first.\n        await new Promise(setTimeout);\n        // In small screen, we toggle keyboard picker.\n        if (this.ui.isSmall) {\n            if (this.props.state.picker === this.props.PICKERS.NONE) {\n                this.props.state.picker = this.props.PICKERS.EMOJI;\n            } else {\n                this.props.state.picker = this.props.PICKERS.NONE;\n            }\n            return;\n        }\n        // In large screen, we toggle popover.\n        if (isEventHandled(ev, \"Composer.onClickAddEmoji\")) {\n            if (this.popover.isOpen) {\n                if (this.props.state.picker === this.props.PICKERS.EMOJI) {\n                    this.props.state.picker = this.props.PICKERS.NONE;\n                    this.popover.close();\n                    return;\n                }\n                this.props.state.picker = this.props.PICKERS.EMOJI;\n            } else {\n                this.props.state.picker = this.props.PICKERS.EMOJI;\n                this.popover.open(el, this.contentProps);\n            }\n        }\n    }\n\n    close() {\n        this.props.close?.();\n        this.popover.close();\n        this.props.state.picker = this.props.PICKERS.NONE;\n        this.props.state.searchTerm = \"\";\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { markEventHandled } from \"@web/core/utils/misc\";\nimport { EmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\n\n/**\n * PickerContent is the content displayed in the popover/Picker.\n * It is used to display the emoji picker/gif picker (if it is enabled).\n */\nexport class PickerContent extends Component {\n    static components = { EmojiPicker };\n    static props = [\"PICKERS\", \"close\", \"pickers\", \"state\", \"storeScroll\"];\n    static template = \"mail.PickerContent\";\n\n    onClick(ev) {\n        markEventHandled(ev, \"PickerContent.onClick\");\n    }\n}\n", "export * from \"@mail/model/export\";\n", "import { Component, onWillDestroy, onWillUpdateProps, xml } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst MINUTE = 60 * 1000;\nconst HOUR = 60 * MINUTE;\n\nexport class RelativeTime extends Component {\n    static props = [\"datetime\"];\n    static template = xml`<t t-esc=\"relativeTime\"/>`;\n\n    setup() {\n        super.setup();\n        this.timeout = null;\n        this.computeRelativeTime(this.props.datetime);\n        onWillDestroy(() => clearTimeout(this.timeout));\n        onWillUpdateProps((nextProps) => {\n            clearTimeout(this.timeout);\n            this.computeRelativeTime(nextProps.datetime);\n        });\n    }\n\n    computeRelativeTime(datetime) {\n        if (!datetime) {\n            this.relativeTime = \"\";\n            return;\n        }\n        const delta = Date.now() - datetime.ts;\n        const absDelta = Math.abs(delta);\n        if (absDelta < 45 * 1000) {\n            this.relativeTime = delta < 0 ? _t(\"in a few seconds\") : _t(\"now\");\n        } else {\n            this.relativeTime = datetime.toRelative();\n        }\n        const updateDelay = absDelta < MINUTE ? absDelta : absDelta < HOUR ? MINUTE : HOUR;\n        this.timeout = setTimeout(() => {\n            this.computeRelativeTime(this.props.datetime);\n            this.render();\n        }, updateDelay);\n    }\n}\n", "import { Component, onWillUpdateProps, useExternalListener, useState } from \"@odoo/owl\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\nimport { useMessageSearch } from \"@mail/core/common/message_search_hook\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\nimport { MessageCardList } from \"./message_card_list\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"@mail/core/common/thread_model\").Thread} thread\n * @property {string} [className]\n * @property {funtion} [closeSearch]\n * @property {funtion} [onClickJump]\n * @extends {Component<Props, Env>}\n */\nexport class SearchMessagesPanel extends Component {\n    static components = {\n        MessageCardList,\n        ActionPanel,\n    };\n    static props = [\"thread\", \"className?\", \"closeSearch?\", \"onClickJump?\"];\n    static template = \"mail.SearchMessagesPanel\";\n\n    setup() {\n        super.setup();\n        this.state = useState({ searchTerm: \"\", searchedTerm: \"\" });\n        this.messageSearch = useMessageSearch(this.props.thread);\n        useAutofocus();\n        useExternalListener(\n            browser,\n            \"keydown\",\n            (ev) => {\n                if (ev.key === \"Escape\") {\n                    this.props.closeSearch?.();\n                }\n            },\n            { capture: true }\n        );\n        onWillUpdateProps((nextProps) => {\n            if (this.props.thread.notEq(nextProps.thread)) {\n                this.env.searchMenu?.close();\n            }\n        });\n    }\n\n    get title() {\n        return _t(\"Search messages\");\n    }\n\n    get MESSAGES_FOUND() {\n        if (this.messageSearch.messages.length === 0) {\n            return false;\n        }\n        return _t(\"%s messages found\", this.messageSearch.count);\n    }\n\n    search() {\n        this.messageSearch.searchTerm = this.state.searchTerm;\n        this.messageSearch.search();\n        this.state.searchedTerm = this.state.searchTerm;\n    }\n\n    clear() {\n        this.state.searchTerm = \"\";\n        this.state.searchedTerm = this.state.searchTerm;\n        this.messageSearch.clear();\n        this.props.closeSearch?.();\n    }\n\n    /** @param {KeyboardEvent} ev */\n    onKeydownSearch(ev) {\n        if (ev.key !== \"Enter\") {\n            return;\n        }\n        if (!this.state.searchTerm) {\n            this.clear();\n        } else {\n            this.search();\n        }\n    }\n\n    onLoadMoreVisible() {\n        const before = this.messageSearch.messages\n            ? Math.min(...this.messageSearch.messages.map((message) => message.id))\n            : false;\n        this.messageSearch.search(before);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { Record } from \"./record\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport class Settings extends Record {\n    id;\n\n    setup() {\n        super.setup();\n        this.saveVoiceThresholdDebounce = debounce(() => {\n            browser.localStorage.setItem(\n                \"mail_user_setting_voice_threshold\",\n                this.voiceActivationThreshold.toString()\n            );\n        }, 2000);\n        this.hasCanvasFilterSupport =\n            typeof document.createElement(\"canvas\").getContext(\"2d\").filter !== \"undefined\";\n        this._loadLocalSettings();\n    }\n\n    // Notification settings\n    /**\n     * @type {\"mentions\"|\"all\"|\"no_notif\"}\n     */\n    channel_notifications = Record.attr(\"mentions\", {\n        compute() {\n            return this.channel_notifications === false ? \"mentions\" : this.channel_notifications;\n        },\n    });\n    mute_until_dt = Record.attr(false, { type: \"datetime\" });\n\n    // Voice settings\n    // DeviceId of the audio input selected by the user\n    audioInputDeviceId = \"\";\n    use_push_to_talk = false;\n    voice_active_duration = 200;\n    volumes = Record.many(\"Volume\");\n    volumeSettingsTimeouts = new Map();\n    // Normalized [0, 1] volume at which the voice activation system must consider the user as \"talking\".\n    voiceActivationThreshold = 0.05;\n    // true if listening to keyboard input to register the push to talk key.\n    isRegisteringKey = false;\n    push_to_talk_key;\n\n    // Video settings\n    backgroundBlurAmount = 10;\n    edgeBlurAmount = 10;\n    showOnlyVideo = false;\n    useBlur = false;\n\n    logRtc = false;\n    /**\n     * @returns {Object} MediaTrackConstraints\n     */\n    get audioConstraints() {\n        const constraints = {\n            echoCancellation: true,\n            noiseSuppression: true,\n        };\n        if (this.audioInputDeviceId) {\n            constraints.deviceId = this.audioInputDeviceId;\n        }\n        return constraints;\n    }\n\n    get NOTIFICATIONS() {\n        return [\n            {\n                label: \"all\",\n                name: _t(\"All Messages\"),\n            },\n            {\n                label: \"mentions\",\n                name: _t(\"Mentions Only\"),\n            },\n            {\n                label: \"no_notif\",\n                name: _t(\"Nothing\"),\n            },\n        ];\n    }\n\n    get MUTES() {\n        return [\n            {\n                label: \"15_mins\",\n                value: 15,\n                name: _t(\"For 15 minutes\"),\n            },\n            {\n                label: \"1_hour\",\n                value: 60,\n                name: _t(\"For 1 hour\"),\n            },\n            {\n                label: \"3_hours\",\n                value: 180,\n                name: _t(\"For 3 hours\"),\n            },\n            {\n                label: \"8_hours\",\n                value: 480,\n                name: _t(\"For 8 hours\"),\n            },\n            {\n                label: \"24_hours\",\n                value: 1440,\n                name: _t(\"For 24 hours\"),\n            },\n            {\n                label: \"forever\",\n                value: -1,\n                name: _t(\"Until I turn it back on\"),\n            },\n        ];\n    }\n\n    getMuteUntilText(dt) {\n        if (dt) {\n            return dt.year <= luxon.DateTime.now().year + 2\n                ? sprintf(_t(`Until %s`), dt.toLocaleString(luxon.DateTime.DATETIME_MED))\n                : _t(\"Until I turn it back on\");\n        }\n        return undefined;\n    }\n\n    /**\n     * @param {string} custom_notifications\n     * @param {import(\"models\").Thread} thread\n     */\n    async setCustomNotifications(custom_notifications, thread = undefined) {\n        return rpc(\"/discuss/settings/custom_notifications\", {\n            custom_notifications:\n                !thread && custom_notifications === \"mentions\" ? false : custom_notifications,\n            channel_id: thread?.id,\n        });\n    }\n\n    /**\n     * @param {integer|false} minutes\n     * @param {import(\"models\").Thread} thread\n     */\n    async setMuteDuration(minutes, thread = undefined) {\n        return rpc(\"/discuss/settings/mute\", {\n            minutes,\n            channel_id: thread?.id,\n        });\n    }\n\n    /**\n     * @param {String} audioInputDeviceId\n     */\n    async setAudioInputDevice(audioInputDeviceId) {\n        this.audioInputDeviceId = audioInputDeviceId;\n        browser.localStorage.setItem(\"mail_user_setting_audio_input_device_id\", audioInputDeviceId);\n    }\n    /**\n     * @param {string} value\n     */\n    setDelayValue(value) {\n        this.voice_active_duration = parseInt(value, 10);\n        this._saveSettings();\n    }\n    /**\n     * @param {event} ev\n     */\n    async setPushToTalkKey(ev) {\n        const nonElligibleKeys = new Set([\"Shift\", \"Control\", \"Alt\", \"Meta\"]);\n        let pushToTalkKey = `${ev.shiftKey || \"\"}.${ev.ctrlKey || ev.metaKey || \"\"}.${\n            ev.altKey || \"\"\n        }`;\n        if (!nonElligibleKeys.has(ev.key)) {\n            pushToTalkKey += `.${ev.key === \" \" ? \"Space\" : ev.key}`;\n        }\n        this.push_to_talk_key = pushToTalkKey;\n        this._saveSettings();\n    }\n    /**\n     * @param {Object} param0\n     * @param {number} [param0.partnerId]\n     * @param {number} [param0.guestId]\n     * @param {number} param0.volume\n     */\n    async saveVolumeSetting({ partnerId, guestId, volume }) {\n        if (this.store.self.type !== \"partner\") {\n            return;\n        }\n        const key = `${partnerId}_${guestId}`;\n        if (this.volumeSettingsTimeouts.get(key)) {\n            browser.clearTimeout(this.volumeSettingsTimeouts.get(key));\n        }\n        this.volumeSettingsTimeouts.set(\n            key,\n            browser.setTimeout(\n                this._onSaveVolumeSettingTimeout.bind(this, { key, partnerId, guestId, volume }),\n                5000\n            )\n        );\n    }\n    /**\n     * @param {float} voiceActivationThreshold\n     */\n    setThresholdValue(voiceActivationThreshold) {\n        this.voiceActivationThreshold = voiceActivationThreshold;\n        this.saveVoiceThresholdDebounce();\n    }\n\n    // methods\n\n    buildKeySet({ shiftKey, ctrlKey, altKey, key }) {\n        const keys = new Set();\n        if (key) {\n            keys.add(key === \"Meta\" ? \"Alt\" : key);\n        }\n        if (shiftKey) {\n            keys.add(\"Shift\");\n        }\n        if (ctrlKey) {\n            keys.add(\"Control\");\n        }\n        if (altKey) {\n            keys.add(\"Alt\");\n        }\n        return keys;\n    }\n\n    /**\n     * @param {event} ev\n     * @param {Object} param1\n     */\n    isPushToTalkKey(ev) {\n        if (!this.use_push_to_talk || !this.push_to_talk_key) {\n            return false;\n        }\n        const [shiftKey, ctrlKey, altKey, key] = this.push_to_talk_key.split(\".\");\n        const settingsKeySet = this.buildKeySet({ shiftKey, ctrlKey, altKey, key });\n        const eventKeySet = this.buildKeySet({\n            shiftKey: ev.shiftKey,\n            ctrlKey: ev.ctrlKey,\n            altKey: ev.altKey,\n            key: ev.key,\n        });\n        if (ev.type === \"keydown\") {\n            return [...settingsKeySet].every((key) => eventKeySet.has(key));\n        }\n        return settingsKeySet.has(ev.key === \"Meta\" ? \"Alt\" : ev.key);\n    }\n    pushToTalkKeyFormat() {\n        if (!this.push_to_talk_key) {\n            return;\n        }\n        const [shiftKey, ctrlKey, altKey, key] = this.push_to_talk_key.split(\".\");\n        return {\n            shiftKey: !!shiftKey,\n            ctrlKey: !!ctrlKey,\n            altKey: !!altKey,\n            key: key || false,\n        };\n    }\n    setPushToTalk(value) {\n        this.use_push_to_talk = value;\n        this._saveSettings();\n    }\n    /**\n     * @private\n     */\n    _loadLocalSettings() {\n        const voiceActivationThresholdString = browser.localStorage.getItem(\n            \"mail_user_setting_voice_threshold\"\n        );\n        this.voiceActivationThreshold = voiceActivationThresholdString\n            ? parseFloat(voiceActivationThresholdString)\n            : this.voiceActivationThreshold;\n        this.audioInputDeviceId = browser.localStorage.getItem(\n            \"mail_user_setting_audio_input_device_id\"\n        );\n        this.showOnlyVideo =\n            browser.localStorage.getItem(\"mail_user_setting_show_only_video\") === \"true\";\n        this.useBlur = browser.localStorage.getItem(\"mail_user_setting_use_blur\") === \"true\";\n        const backgroundBlurAmount = browser.localStorage.getItem(\n            \"mail_user_setting_background_blur_amount\"\n        );\n        this.backgroundBlurAmount = backgroundBlurAmount ? parseInt(backgroundBlurAmount) : 10;\n        const edgeBlurAmount = browser.localStorage.getItem(\"mail_user_setting_edge_blur_amount\");\n        this.edgeBlurAmount = edgeBlurAmount ? parseInt(edgeBlurAmount) : 10;\n    }\n    /**\n     * @private\n     */\n    async _onSaveGlobalSettingsTimeout() {\n        this.globalSettingsTimeout = undefined;\n        await this.store.env.services.orm.call(\n            \"res.users.settings\",\n            \"set_res_users_settings\",\n            [[this.id]],\n            {\n                new_settings: {\n                    push_to_talk_key: this.push_to_talk_key,\n                    use_push_to_talk: this.use_push_to_talk,\n                    voice_active_duration: this.voice_active_duration,\n                },\n            }\n        );\n    }\n    /**\n     * @param {Object} param0\n     * @param {String} param0.key\n     * @param {number} [param0.partnerId]\n     * @param {number} param0.volume\n     */\n    async _onSaveVolumeSettingTimeout({ key, partnerId, guestId, volume }) {\n        this.volumeSettingsTimeouts.delete(key);\n        await this.store.env.services.orm.call(\n            \"res.users.settings\",\n            \"set_volume_setting\",\n            [[this.id], partnerId, volume],\n            { guest_id: guestId }\n        );\n    }\n    /**\n     * @private\n     */\n    async _saveSettings() {\n        if (this.store.self.type !== \"partner\") {\n            return;\n        }\n        browser.clearTimeout(this.globalSettingsTimeout);\n        this.globalSettingsTimeout = browser.setTimeout(\n            () => this._onSaveGlobalSettingsTimeout(),\n            2000\n        );\n    }\n}\n\nSettings.register();\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { url } from \"@web/core/utils/urls\";\n\nexport class SoundEffects {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     */\n    constructor(env) {\n        this.soundEffects = {\n            \"channel-join\": { defaultVolume: 0.3, path: \"/mail/static/src/audio/channel_01_in\" },\n            \"channel-leave\": { path: \"/mail/static/src/audio/channel_04_out\" },\n            deafen: { defaultVolume: 0.15, path: \"/mail/static/src/audio/deafen_new_01\" },\n            \"incoming-call\": { defaultVolume: 0.15, path: \"/mail/static/src/audio/call_02_in_\" },\n            \"member-leave\": { defaultVolume: 0.5, path: \"/mail/static/src/audio/channel_01_out\" },\n            mute: { defaultVolume: 0.2, path: \"/mail/static/src/audio/mute_1\" },\n            \"new-message\": { path: \"/mail/static/src/audio/dm_02\" },\n            \"push-to-talk-on\": { defaultVolume: 0.05, path: \"/mail/static/src/audio/ptt_push_1\" },\n            \"push-to-talk-off\": {\n                defaultVolume: 0.05,\n                path: \"/mail/static/src/audio/ptt_release_1\",\n            },\n            \"screen-sharing\": { defaultVolume: 0.5, path: \"/mail/static/src/audio/share_02\" },\n            undeafen: { defaultVolume: 0.15, path: \"/mail/static/src/audio/undeafen_new_01\" },\n            unmute: { defaultVolume: 0.2, path: \"/mail/static/src/audio/unmute_1\" },\n        };\n    }\n\n    /**\n     * @param {String} param0 soundEffectName\n     * @param {Object} param1\n     * @param {boolean} [param1.loop] true if we want to make the audio loop, will only stop if stop() is called\n     * @param {float} [param1.volume] the volume percentage in decimal to play this sound.\n     *   If not provided, uses the default volume of this sound effect.\n     */\n    play(soundEffectName, { loop = false, volume } = {}) {\n        if (typeof browser.Audio === \"undefined\") {\n            return;\n        }\n        const soundEffect = this.soundEffects[soundEffectName];\n        if (!soundEffect) {\n            return;\n        }\n        if (!soundEffect.audio) {\n            const audio = new browser.Audio();\n            const ext = audio.canPlayType(\"audio/ogg; codecs=vorbis\") ? \".ogg\" : \".mp3\";\n            audio.src = url(soundEffect.path + ext);\n            soundEffect.audio = audio;\n        }\n        if (!soundEffect.audio.paused) {\n            soundEffect.audio.pause();\n        }\n        soundEffect.audio.currentTime = 0;\n        soundEffect.audio.loop = loop;\n        soundEffect.audio.volume = volume ?? soundEffect.defaultVolume ?? 1;\n        Promise.resolve(soundEffect.audio.play()).catch(() => {});\n    }\n    /**\n     * Resets the audio to the start of the track and pauses it.\n     * @param {String} [soundEffectName]\n     */\n    stop(soundEffectName) {\n        const soundEffect = this.soundEffects[soundEffectName];\n        if (soundEffect) {\n            if (soundEffect.audio) {\n                soundEffect.audio.pause();\n                soundEffect.audio.currentTime = 0;\n            }\n        } else {\n            for (const soundEffect of Object.values(this.soundEffects)) {\n                if (soundEffect.audio) {\n                    soundEffect.audio.pause();\n                    soundEffect.audio.currentTime = 0;\n                }\n            }\n        }\n    }\n}\n\nexport const soundEffects = {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     */\n    start(env) {\n        return new SoundEffects(env);\n    },\n};\n\nregistry.category(\"services\").add(\"mail.sound_effects\", soundEffects);\n", "import { compareDatetime } from \"@mail/utils/common/misc\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { Store as BaseStore, makeStore, Record } from \"@mail/core/common/record\";\nimport { reactive } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { Deferred, Mutex } from \"@web/core/utils/concurrency\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { session } from \"@web/session\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { cleanTerm, prettifyMessageContent } from \"@mail/utils/common/format\";\n\n/**\n * @typedef {{isSpecial: boolean, channel_types: string[], label: string, displayName: string, description: string}} SpecialMention\n */\n\nlet prevLastMessageId = null;\nlet temporaryIdOffset = 0.01;\n\nexport const pyToJsModels = {\n    \"discuss.channel.member\": \"ChannelMember\",\n    \"discuss.channel.rtc.session\": \"RtcSession\",\n    \"discuss.channel\": \"Thread\",\n    \"ir.attachment\": \"Attachment\",\n    \"mail.activity\": \"Activity\",\n    \"mail.guest\": \"Persona\",\n    \"mail.followers\": \"Follower\",\n    \"mail.link.preview\": \"LinkPreview\",\n    \"mail.message\": \"Message\",\n    \"mail.notification\": \"Notification\",\n    \"mail.scheduled.message\": \"ScheduledMessage\",\n    \"mail.thread\": \"Thread\",\n    \"res.partner\": \"Persona\",\n};\n\nexport const addFieldsByPyModel = {\n    \"discuss.channel\": { model: \"discuss.channel\" },\n    \"mail.guest\": { type: \"guest\" },\n    \"res.partner\": { type: \"partner\" },\n};\n\nexport class Store extends BaseStore {\n    static FETCH_DATA_DEBOUNCE_DELAY = 1;\n    static OTHER_LONG_TYPING = 60000;\n    FETCH_LIMIT = 30;\n    DEFAULT_AVATAR = \"/mail/static/src/img/smiley/avatar.jpg\";\n    isReady = new Deferred();\n\n    /** @returns {import(\"models\").Store|import(\"models\").Store[]} */\n    static insert() {\n        return super.insert(...arguments);\n    }\n\n    /** @type {typeof import(\"@mail/core/web/activity_model\").Activity} */\n    Activity;\n    /** @type {typeof import(\"@mail/core/common/attachment_model\").Attachment} */\n    Attachment;\n    /** @type {typeof import(\"@mail/core/common/canned_response_model\").CannedResponse} */\n    [\"mail.canned.response\"];\n    /** @type {typeof import(\"@mail/core/common/channel_member_model\").ChannelMember} */\n    ChannelMember;\n    /** @type {typeof import(\"@mail/core/common/chat_window_model\").ChatWindow} */\n    ChatWindow;\n    /** @type {typeof import(\"@mail/core/common/composer_model\").Composer} */\n    Composer;\n    /** @type {typeof import(\"@mail/core/common/failure_model\").Failure} */\n    Failure;\n    /** @type {typeof import(\"@mail/core/common/follower_model\").Follower} */\n    Follower;\n    /** @type {typeof import(\"@mail/core/common/link_preview_model\").LinkPreview} */\n    LinkPreview;\n    /** @type {typeof import(\"@mail/core/common/message_model\").Message} */\n    Message;\n    /** @type {typeof import(\"@mail/core/common/message_reactions_model\").MessageReactions} */\n    MessageReactions;\n    /** @type {typeof import(\"@mail/core/common/notification_model\").Notification} */\n    Notification;\n    /** @type {typeof import(\"@mail/core/common/persona_model\").Persona} */\n    Persona;\n    /** @type {typeof import \"@mail/chatter/web/scheduled_message_model).ScheduledMessage\"} */\n    ScheduledMessage;\n    /** @type {typeof import(\"@mail/core/common/settings_model\").Settings} */\n    Settings;\n    /** @type {typeof import(\"@mail/core/common/thread_model\").Thread} */\n    Thread;\n    /** @type {typeof import(\"@mail/core/common/volume_model\").Volume} */\n    Volume;\n\n    /**\n     * Defines channel types that have the message seen indicator/info feature.\n     * @see `discuss.channel`._types_allowing_seen_infos()\n     *\n     * @type {string[]}\n     */\n    channel_types_with_seen_infos = [];\n    /** This is the current logged partner / guest */\n    self = Record.one(\"Persona\");\n    /**\n     * Indicates whether the current user is using the application through the\n     * public page.\n     */\n    inPublicPage = false;\n    odoobot = Record.one(\"Persona\");\n    /** @type {boolean} */\n    odoobotOnboarding;\n    users = {};\n    /** @type {number} */\n    internalUserGroupId;\n    /** @type {number} */\n    mt_comment_id;\n    /** @type {boolean} */\n    hasMessageTranslationFeature;\n    imStatusTrackedPersonas = Record.many(\"Persona\", {\n        inverse: \"storeAsTrackedImStatus\",\n    });\n    hasLinkPreviewFeature = true;\n    // messaging menu\n    menu = { counter: 0 };\n    chatHub = Record.one(\"ChatHub\", { compute: () => ({}) });\n    failures = Record.many(\"Failure\", {\n        /**\n         * @param {import(\"models\").Failure} f1\n         * @param {import(\"models\").Failure} f2\n         */\n        sort: (f1, f2) => f2.lastMessage?.id - f1.lastMessage?.id,\n    });\n    settings = Record.one(\"Settings\");\n    openInviteThread = Record.one(\"Thread\");\n\n    fetchDeferred = new Deferred();\n    fetchParams = {};\n    fetchReadonly = true;\n    fetchSilent = true;\n\n    cannedReponses = this.makeCachedFetchData({ canned_responses: true });\n\n    specialMentions = [\n        {\n            isSpecial: true,\n            label: \"everyone\",\n            channel_types: [\"channel\", \"group\"],\n            displayName: \"Everyone\",\n            description: _t(\"Notify everyone\"),\n        },\n    ];\n\n    get initMessagingParams() {\n        return {\n            init_messaging: {},\n        };\n    }\n\n    messagePostMutex = new Mutex();\n\n    menuThreads = Record.many(\"Thread\", {\n        /** @this {import(\"models\").Store} */\n        compute() {\n            /** @type {import(\"models\").Thread[]} */\n            const searchTerm = cleanTerm(this.discuss.searchTerm);\n            let threads = Object.values(this.Thread.records).filter(\n                (thread) =>\n                    (thread.displayToSelf ||\n                        (thread.needactionMessages.length > 0 && thread.model !== \"mail.box\")) &&\n                    cleanTerm(thread.displayName).includes(searchTerm)\n            );\n            const tab = this.discuss.activeTab;\n            if (tab !== \"main\") {\n                threads = threads.filter(({ channel_type }) =>\n                    this.tabToThreadType(tab).includes(channel_type)\n                );\n            } else if (tab === \"main\" && this.env.inDiscussApp) {\n                threads = threads.filter(({ channel_type }) =>\n                    this.tabToThreadType(\"mailbox\").includes(channel_type)\n                );\n            }\n            return threads;\n        },\n        /**\n         * @this {import(\"models\").Store}\n         * @param {import(\"models\").Thread} a\n         * @param {import(\"models\").Thread} b\n         */\n        sort(a, b) {\n            /**\n             * Ordering:\n             * - threads with needaction\n             * - unread channels\n             * - read channels\n             * - odoobot chat\n             *\n             * In each group, thread with most recent message comes first\n             */\n            const aOdooBot = a.isCorrespondentOdooBot;\n            const bOdooBot = b.isCorrespondentOdooBot;\n            if (aOdooBot && !bOdooBot) {\n                return 1;\n            }\n            if (bOdooBot && !aOdooBot) {\n                return -1;\n            }\n            const aNeedaction = a.needactionMessages.length;\n            const bNeedaction = b.needactionMessages.length;\n            if (aNeedaction > 0 && bNeedaction === 0) {\n                return -1;\n            }\n            if (bNeedaction > 0 && aNeedaction === 0) {\n                return 1;\n            }\n            const aUnread = a.selfMember?.message_unread_counter;\n            const bUnread = b.selfMember?.message_unread_counter;\n            if (aUnread > 0 && bUnread === 0) {\n                return -1;\n            }\n            if (bUnread > 0 && aUnread === 0) {\n                return 1;\n            }\n            const aMessageDatetime = a.newestPersistentNotEmptyOfAllMessage?.datetime;\n            const bMessageDateTime = b.newestPersistentNotEmptyOfAllMessage?.datetime;\n            if (!aMessageDatetime && bMessageDateTime) {\n                return 1;\n            }\n            if (!bMessageDateTime && aMessageDatetime) {\n                return -1;\n            }\n            if (aMessageDatetime && bMessageDateTime && aMessageDatetime !== bMessageDateTime) {\n                return bMessageDateTime - aMessageDatetime;\n            }\n            return b.localId > a.localId ? 1 : -1;\n        },\n    });\n\n    /**\n     * @param {Object} params post message data\n     * @param {import(\"models\").Message} tmpMessage the associated temporary message\n     */\n    async doMessagePost(params, tmpMessage) {\n        return this.messagePostMutex.exec(async () => {\n            let res;\n            try {\n                res = await rpc(\"/mail/message/post\", params, { silent: true });\n            } catch (err) {\n                if (!tmpMessage) {\n                    throw err;\n                }\n                tmpMessage.postFailRedo = () => {\n                    tmpMessage.postFailRedo = undefined;\n                    tmpMessage.thread.messages.delete(tmpMessage);\n                    tmpMessage.thread.messages.add(tmpMessage);\n                    this.doMessagePost(params, tmpMessage);\n                };\n            }\n            return res;\n        });\n    }\n\n    /**\n     * @returns {Deferred}\n     */\n    async fetchData(params, { readonly = true, silent = true } = {}) {\n        Object.assign(this.fetchParams, params);\n        this.fetchReadonly = this.fetchReadonly && readonly;\n        this.fetchSilent = this.fetchSilent && silent;\n        const fetchDeferred = this.fetchDeferred;\n        this._fetchDataDebounced();\n        return fetchDeferred;\n    }\n\n    /** Import data received from init_messaging */\n    async initialize() {\n        await this.fetchData(this.initMessagingParams, { readonly: false });\n        this.isReady.resolve();\n    }\n\n    /**\n     * Create a cacheable version of the `fetchData` method. The result of the\n     * request is cached once acquired. In case of failure, the deferred is\n     * rejected and the cache is reset allowing to retry the request when\n     * calling the function again.\n     *\n     * @param {{[key: string]: boolean}} params Parameters to pass to the `fetchData` method.\n     * @returns {{\n     *      fetch: () => ReturnType<Store[\"fetchData\"]>,\n     *      status: \"not_fetched\"|\"fetching\"|\"fetched\"\n     * }}\n     */\n    makeCachedFetchData(params) {\n        let def = null;\n        const r = reactive({\n            status: \"not_fetched\",\n            fetch: () => {\n                if ([\"fetching\", \"fetched\"].includes(r.status)) {\n                    return def;\n                }\n                r.status = \"fetching\";\n                def = new Deferred();\n                this.fetchData(params).then(\n                    (result) => {\n                        r.status = \"fetched\";\n                        def.resolve(result);\n                    },\n                    (error) => {\n                        r.status = \"not_fetched\";\n                        def.reject(error);\n                    }\n                );\n                return def;\n            },\n        });\n        return r;\n    }\n\n    async _fetchDataDebounced() {\n        const fetchDeferred = this.fetchDeferred;\n        this.fetchParams.context = {\n            ...user.context,\n            ...this.fetchParams.context,\n        };\n        rpc(this.fetchReadonly ? \"/mail/data\" : \"/mail/action\", this.fetchParams, {\n            silent: this.fetchSilent,\n        }).then(\n            (data) => {\n                const recordsByModel = this.insert(data, { html: true });\n                fetchDeferred.resolve(recordsByModel);\n            },\n            (error) => fetchDeferred.reject(error)\n        );\n        this.fetchDeferred = new Deferred();\n        this.fetchParams = {};\n        this.fetchReadonly = true;\n        this.fetchSilent = true;\n    }\n\n    /**\n     * @template T\n     * @param {T} [dataByModelName={}]\n     * @param {Object} [options={}]\n     * @returns {{ [K in keyof T]: import(\"models\").Models[K][] }}\n     */\n    insert(dataByModelName = {}, options = {}) {\n        const store = this;\n        const pyModels = Object.values(pyToJsModels);\n        return Record.MAKE_UPDATE(function storeInsert() {\n            const res = {};\n            const recordsDataToDelete = [];\n            for (const [pyOrJsModelName, data] of Object.entries(dataByModelName)) {\n                if (pyModels.includes(pyOrJsModelName)) {\n                    console.warn(\n                        `store.insert() should receive the python model name instead of \u201c${pyOrJsModelName}\u201d.`\n                    );\n                }\n                const modelName = pyToJsModels[pyOrJsModelName] || pyOrJsModelName;\n                if (!store[modelName]) {\n                    console.warn(`store.insert() received data for unknown model \u201c${modelName}\u201d.`);\n                    continue;\n                }\n                const insertData = [];\n                for (const vals of Array.isArray(data) ? data : [data]) {\n                    const extraFields = addFieldsByPyModel[pyOrJsModelName];\n                    if (extraFields) {\n                        Object.assign(vals, extraFields);\n                    }\n                    if (vals._DELETE) {\n                        delete vals._DELETE;\n                        recordsDataToDelete.push([modelName, vals]);\n                    } else {\n                        insertData.push(vals);\n                    }\n                }\n                const records = store[modelName].insert(insertData, options);\n                if (!res[modelName]) {\n                    res[modelName] = records;\n                } else {\n                    const knownRecordIds = new Set(res[modelName].map((r) => r.localId));\n                    res[modelName].push(...records.filter((r) => !knownRecordIds.has(r.localId)));\n                }\n            }\n            // Delete after all inserts to make sure a relation potentially registered before the\n            // delete doesn't re-add the deleted record by mistake.\n            for (const [modelName, vals] of recordsDataToDelete) {\n                store[modelName].get(vals)?.delete();\n            }\n            return res;\n        });\n    }\n\n    async startMeeting() {\n        const thread = await this.env.services[\"discuss.core.common\"].createGroupChat({\n            default_display_mode: \"video_full_screen\",\n            partners_to: [this.self.id],\n        });\n        this.ChatWindow.get(thread)?.update({ autofocus: 0 });\n        this.env.services[\"discuss.rtc\"].toggleCall(thread, { camera: true });\n        this.openInviteThread = thread;\n    }\n\n    /**\n     * @param {'chat' | 'group'} tab\n     * @returns Thread types matching the given tab.\n     */\n    tabToThreadType(tab) {\n        return tab === \"chat\" ? [\"chat\", \"group\"] : [tab];\n    }\n\n    handleClickOnLink(ev, thread) {\n        const model = ev.target.dataset.oeModel;\n        const id = Number(ev.target.dataset.oeId);\n        if (ev.target.closest(\".o_channel_redirect\") && model && id) {\n            ev.preventDefault();\n            this.Thread.getOrFetch({ model, id }).then((thread) => {\n                if (thread) {\n                    thread.open();\n                }\n            });\n            return true;\n        } else if (ev.target.closest(\".o_mail_redirect\") && id) {\n            ev.preventDefault();\n            this.openChat({ partnerId: id });\n            return true;\n        } else if (ev.target.tagName === \"A\" && model && id) {\n            ev.preventDefault();\n            Promise.resolve(\n                this.env.services.action.doAction({\n                    type: \"ir.actions.act_window\",\n                    res_model: model,\n                    views: [[false, \"form\"]],\n                    res_id: id,\n                })\n            ).then(() => this.onLinkFollowed(thread));\n            return true;\n        }\n        return false;\n    }\n\n    onLinkFollowed(fromThread) {}\n\n    setup() {\n        super.setup();\n        this._fetchDataDebounced = debounce(\n            this._fetchDataDebounced,\n            Store.FETCH_DATA_DEBOUNCE_DELAY\n        );\n        this.updateBusSubscription = debounce(\n            () => this.env.services.bus_service.forceUpdateChannels(),\n            0\n        );\n    }\n\n    /** Provides an override point for when the store service has started. */\n    onStarted() {}\n\n    /**\n     * Search and fetch for a partner with a given user or partner id.\n     * @param {Object} param0\n     * @param {number} param0.userId\n     * @param {number} param0.partnerId\n     * @returns {Promise<import(\"models\").Thread | undefined>}\n     */\n    async getChat({ userId, partnerId }) {\n        const partner = await this.getPartner({ userId, partnerId });\n        let chat = partner?.searchChat();\n        if (!chat || !chat.is_pinned) {\n            chat = await this.joinChat(partnerId || partner?.id);\n        }\n        if (!chat) {\n            this.env.services.notification.add(\n                _t(\"An unexpected error occurred during the creation of the chat.\"),\n                { type: \"warning\" }\n            );\n            return;\n        }\n        return chat;\n    }\n\n    /** @returns {number} */\n    getLastMessageId() {\n        return Object.values(this.Message.records).reduce(\n            (lastMessageId, message) => Math.max(lastMessageId, message.id),\n            0\n        );\n    }\n\n    getMentionsFromText(\n        body,\n        { mentionedChannels = [], mentionedPartners = [], specialMentions = [] } = {}\n    ) {\n        const validMentions = {};\n        validMentions.threads = mentionedChannels.filter((thread) => {\n            if (thread.parent_channel_id) {\n                return body.includes(\n                    `#${thread.parent_channel_id.displayName} > ${thread.displayName}`\n                );\n            }\n            return body.includes(`#${thread.displayName}`);\n        });\n        validMentions.partners = mentionedPartners.filter((partner) =>\n            body.includes(`@${partner.name}`)\n        );\n        validMentions.specialMentions = this.specialMentions\n            .filter((special) => body.includes(`@${special.label}`))\n            .map((special) => special.label);\n        return validMentions;\n    }\n\n    /**\n     * Get the parameters to pass to the message post route.\n     */\n    async getMessagePostParams({ body, postData, thread }) {\n        const { attachments, cannedResponseIds, isNote, mentionedChannels, mentionedPartners } =\n            postData;\n        const subtype = isNote ? \"mail.mt_note\" : \"mail.mt_comment\";\n        const validMentions = this.getMentionsFromText(body, {\n            mentionedChannels,\n            mentionedPartners,\n        });\n        const partner_ids = validMentions?.partners.map((partner) => partner.id) ?? [];\n        const recipientEmails = [];\n        const recipientAdditionalValues = {};\n        if (!isNote) {\n            const recipientIds = thread.suggestedRecipients\n                .filter((recipient) => recipient.persona && recipient.checked)\n                .map((recipient) => recipient.persona.id);\n            thread.suggestedRecipients\n                .filter((recipient) => recipient.checked && !recipient.persona)\n                .forEach((recipient) => {\n                    recipientEmails.push(recipient.email);\n                    recipientAdditionalValues[recipient.email] = recipient.create_values;\n                });\n            partner_ids.push(...recipientIds);\n        }\n        postData = {\n            body: await prettifyMessageContent(body, validMentions),\n            message_type: \"comment\",\n            subtype_xmlid: subtype,\n        };\n        if (attachments.length) {\n            postData.attachment_ids = attachments.map(({ id }) => id);\n        }\n        if (partner_ids.length) {\n            Object.assign(postData, { partner_ids });\n        }\n        if (thread.model === \"discuss.channel\" && validMentions?.specialMentions.length) {\n            postData.special_mentions = validMentions.specialMentions;\n        }\n        const params = {\n            context: {\n                mail_post_autofollow: !isNote && thread.hasWriteAccess,\n            },\n            post_data: postData,\n            thread_id: thread.id,\n            thread_model: thread.model,\n        };\n        if (attachments.length) {\n            params.attachment_tokens = attachments.map((attachment) => attachment.access_token);\n        }\n        if (cannedResponseIds?.length) {\n            params.canned_response_ids = cannedResponseIds;\n        }\n        if (recipientEmails.length) {\n            Object.assign(params, {\n                partner_emails: recipientEmails,\n                partner_additional_values: recipientAdditionalValues,\n            });\n        }\n        return params;\n    }\n\n    getNextTemporaryId() {\n        const lastMessageId = this.getLastMessageId();\n        if (prevLastMessageId === lastMessageId) {\n            temporaryIdOffset += 0.01;\n        } else {\n            prevLastMessageId = lastMessageId;\n            temporaryIdOffset = 0.01;\n        }\n        return lastMessageId + temporaryIdOffset;\n    }\n\n    /**\n     * Search and fetch for a partner with a given user or partner id.\n     * @param {Object} param0\n     * @param {number} param0.userId\n     * @param {number} param0.partnerId\n     * @returns {Promise<import(\"models\").Persona> | undefined}\n     */\n    async getPartner({ userId, partnerId }) {\n        if (userId) {\n            let user = this.users[userId];\n            if (!user) {\n                this.users[userId] = { id: userId };\n                user = this.users[userId];\n            }\n            if (!user.partner_id) {\n                const [userData] = await this.env.services.orm.silent.read(\n                    \"res.users\",\n                    [user.id],\n                    [\"partner_id\"],\n                    { context: { active_test: false } }\n                );\n                if (userData) {\n                    user.partner_id = userData.partner_id[0];\n                }\n            }\n            if (!user.partner_id) {\n                this.env.services.notification.add(_t(\"You can only chat with existing users.\"), {\n                    type: \"warning\",\n                });\n                return;\n            }\n            partnerId = user.partner_id;\n        }\n        if (partnerId) {\n            const partner = this.Persona.insert({ id: partnerId, type: \"partner\" });\n            if (!partner.userId) {\n                const [userId] = await this.env.services.orm.silent.search(\n                    \"res.users\",\n                    [[\"partner_id\", \"=\", partnerId]],\n                    { context: { active_test: false } }\n                );\n                if (!userId) {\n                    this.env.services.notification.add(\n                        _t(\"You can only chat with partners that have a dedicated user.\"),\n                        { type: \"info\" }\n                    );\n                    return;\n                }\n                partner.userId = userId;\n            }\n            return partner;\n        }\n    }\n\n    /**\n     * List of known partner ids with a direct chat, ordered\n     * by most recent interest (1st item being the most recent)\n     *\n     * @returns {[integer]}\n     */\n    getRecentChatPartnerIds() {\n        return Object.values(this.Thread.records)\n            .filter((thread) => thread.channel_type === \"chat\" && thread.correspondent)\n            .sort((a, b) => compareDatetime(b.lastInterestDt, a.lastInterestDt) || b.id - a.id)\n            .map((thread) => thread.correspondent.persona.id);\n    }\n\n    async joinChannel(id, name) {\n        await this.env.services.orm.call(\"discuss.channel\", \"add_members\", [[id]], {\n            partner_ids: [this.self.id],\n        });\n        const thread = this.Thread.insert({\n            channel_type: \"channel\",\n            id,\n            model: \"discuss.channel\",\n            name,\n        });\n        if (!thread.avatarCacheKey) {\n            thread.avatarCacheKey = \"hello\";\n        }\n        thread.open();\n        return thread;\n    }\n\n    async joinChat(id, forceOpen = false) {\n        const data = await this.env.services.orm.call(\"discuss.channel\", \"channel_get\", [], {\n            partners_to: [id],\n            force_open: forceOpen,\n        });\n        const { Thread } = this.store.insert(data);\n        return Thread[0];\n    }\n\n    async openChat(person) {\n        const chat = await this.getChat(person);\n        chat?.open();\n    }\n\n    openDocument({ id, model }) {\n        this.env.services.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: model,\n            views: [[false, \"form\"]],\n            res_id: id,\n        });\n    }\n\n    openNewMessage() {\n        let cw = this.ChatWindow.get({ thread: undefined });\n        if (cw) {\n            cw.focus();\n            return;\n        }\n        cw = this.ChatWindow.insert({ thread: undefined, fromMessagingMenu: true });\n        this.chatHub.opened.unshift(cw);\n        cw.focus();\n    }\n\n    /**\n     * @param {string} searchTerm\n     * @param {Thread} thread\n     * @param {number|false} [before]\n     */\n    async search(searchTerm, thread, before = false) {\n        const { count, data, messages } = await rpc(thread.getFetchRoute(), {\n            ...thread.getFetchParams(),\n            search_term: await prettifyMessageContent(searchTerm), // formatted like message_post\n            before,\n        });\n        this.insert(data, { html: true });\n        return {\n            count,\n            loadMore: messages.length === this.FETCH_LIMIT,\n            messages: this.Message.insert(messages),\n        };\n    }\n\n    async searchPartners(searchStr = \"\", limit = 10) {\n        const partners = [];\n        const searchTerm = cleanTerm(searchStr);\n        for (const localId in this.Persona.records) {\n            const persona = this.Persona.records[localId];\n            if (persona.type !== \"partner\") {\n                continue;\n            }\n            const partner = persona;\n            if (\n                partner.name &&\n                cleanTerm(partner.name).includes(searchTerm) &&\n                ((partner.active && partner.userId) || partner === this.store.odoobot)\n            ) {\n                partners.push(partner);\n                if (partners.length >= limit) {\n                    break;\n                }\n            }\n        }\n        if (!partners.length) {\n            const data = await this.env.services.orm.silent.call(\"res.partner\", \"im_search\", [\n                searchTerm,\n                limit,\n            ]);\n            const { Persona = [] } = this.store.insert(data);\n            partners.push(...Persona);\n        }\n        return partners;\n    }\n}\nStore.register();\n\nexport const storeService = {\n    dependencies: [\"bus_service\", \"ui\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    start(env, services) {\n        const store = makeStore(env);\n        store.insert(session.storeData);\n        /**\n         * Add defaults for `self` and `settings` because in livechat there could be no user and no\n         * guest yet (both undefined at init), but some parts of the code that loosely depend on\n         * these values will still be executed immediately. Providing a dummy default is enough to\n         * avoid crashes, the actual values being filled at livechat init when they are necessary.\n         */\n        store.self ??= { id: -1, type: \"guest\" };\n        store.settings ??= {};\n        store.initialize();\n        store.onStarted();\n        return store;\n    },\n};\n\nregistry.category(\"services\").add(\"mail.store\", storeService);\n", "import { useSequential } from \"@mail/utils/common/hooks\";\nimport { status, useComponent, useEffect, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\nclass UseSuggestion {\n    constructor(comp) {\n        this.comp = comp;\n        useEffect(\n            (delimiter, position, term) => {\n                this.update();\n                if (this.search.position === undefined || !this.search.delimiter) {\n                    return; // nothing else to fetch\n                }\n                if (this.composer.store.self.type !== \"partner\") {\n                    return; // guests cannot access fetch suggestion method\n                }\n                this.sequential(async () => {\n                    if (\n                        this.search.delimiter !== delimiter ||\n                        this.search.position !== position ||\n                        this.search.term !== term\n                    ) {\n                        return; // ignore obsolete call\n                    }\n                    if (\n                        this.lastFetchedSearch?.count === 0 &&\n                        (!this.search.delimiter || this.isSearchMoreSpecificThanLastFetch)\n                    ) {\n                        return; // no need to fetch since this is more specific than last and last had no result\n                    }\n                    this.state.isFetching = true;\n                    try {\n                        await this.suggestionService.fetchSuggestions(this.search, {\n                            thread: this.thread,\n                        });\n                    } catch {\n                        this.lastFetchedSearch = null;\n                    } finally {\n                        this.state.isFetching = false;\n                    }\n                    if (status(comp) === \"destroyed\") {\n                        return;\n                    }\n                    this.update();\n                    this.lastFetchedSearch = {\n                        ...this.search,\n                        count: this.state.items?.suggestions.length ?? 0,\n                    };\n                    if (\n                        this.search.delimiter === delimiter &&\n                        this.search.position === position &&\n                        this.search.term === term &&\n                        !this.state.items?.suggestions.length\n                    ) {\n                        this.clearSearch();\n                    }\n                });\n            },\n            () => {\n                return [this.search.delimiter, this.search.position, this.search.term];\n            }\n        );\n        useEffect(\n            () => {\n                this.detect();\n            },\n            () => [this.composer.selection.start, this.composer.selection.end, this.composer.text]\n        );\n    }\n    /** @type {import(\"@mail/core/common/composer\").Composer} */\n    comp;\n    get composer() {\n        return this.comp.props.composer;\n    }\n    sequential = useSequential();\n    suggestionService = useService(\"mail.suggestion\");\n    state = useState({\n        count: 0,\n        items: undefined,\n        isFetching: false,\n    });\n    search = {\n        delimiter: undefined,\n        position: undefined,\n        term: \"\",\n    };\n    lastFetchedSearch;\n    get isSearchMoreSpecificThanLastFetch() {\n        return (\n            this.lastFetchedSearch.delimiter === this.search.delimiter &&\n            this.search.term.startsWith(this.lastFetchedSearch.term) &&\n            this.lastFetchedSearch.position >= this.search.position\n        );\n    }\n    clearRawMentions() {\n        this.composer.mentionedChannels.length = 0;\n        this.composer.mentionedPartners.length = 0;\n    }\n    clearCannedResponses() {\n        this.composer.cannedResponses = [];\n    }\n    clearSearch() {\n        Object.assign(this.search, {\n            delimiter: undefined,\n            position: undefined,\n            term: \"\",\n        });\n        this.state.items = undefined;\n    }\n    detect() {\n        const { start, end } = this.composer.selection;\n        const text = this.composer.text;\n        if (start !== end) {\n            // avoid interfering with multi-char selection\n            this.clearSearch();\n            return;\n        }\n        const candidatePositions = [];\n        // consider the chars before the current cursor position\n        let numberOfSpaces = 0;\n        for (let index = start - 1; index >= 0; --index) {\n            if (/\\s/.test(text[index])) {\n                numberOfSpaces++;\n                if (numberOfSpaces === 2) {\n                    // The consideration stops after the second space since\n                    // a majority of partners have a two-word name. This\n                    // removes the need to check for mentions following a\n                    // delimiter used earlier in the content.\n                    break;\n                }\n            }\n            candidatePositions.push(index);\n        }\n        // keep the current delimiter if it is still valid\n        if (this.search.position !== undefined && this.search.position < start) {\n            candidatePositions.push(this.search.position);\n        }\n        const supportedDelimiters = this.suggestionService.getSupportedDelimiters(this.thread);\n        for (const candidatePosition of candidatePositions) {\n            if (candidatePosition < 0 || candidatePosition >= text.length) {\n                continue;\n            }\n            const candidateChar = text[candidatePosition];\n            if (\n                !supportedDelimiters.find(\n                    ([delimiter, allowedPosition]) =>\n                        delimiter === candidateChar &&\n                        (allowedPosition === undefined || allowedPosition === candidatePosition)\n                )\n            ) {\n                continue;\n            }\n            const charBeforeCandidate = text[candidatePosition - 1];\n            if (charBeforeCandidate && !/\\s/.test(charBeforeCandidate)) {\n                continue;\n            }\n            Object.assign(this.search, {\n                delimiter: candidateChar,\n                position: candidatePosition,\n                term: text.substring(candidatePosition + 1, start),\n            });\n            this.state.count++;\n            return;\n        }\n        this.clearSearch();\n    }\n    get thread() {\n        return this.composer.thread || this.composer.message.thread;\n    }\n    insert(option) {\n        const position = this.composer.selection.start;\n        const text = this.composer.text;\n        let before = text.substring(0, this.search.position + 1);\n        let after = text.substring(position, text.length);\n        if (this.search.delimiter === \":\") {\n            before = text.substring(0, this.search.position);\n            after = text.substring(position, text.length);\n        }\n        if (option.partner) {\n            this.composer.mentionedPartners.add({\n                id: option.partner.id,\n                type: \"partner\",\n            });\n        }\n        if (option.thread) {\n            this.composer.mentionedChannels.add({\n                model: \"discuss.channel\",\n                id: option.thread.id,\n            });\n        }\n        if (option.cannedResponse) {\n            this.composer.cannedResponses.push(option.cannedResponse);\n        }\n        this.clearSearch();\n        this.composer.text = before + option.label + \" \" + after;\n        this.composer.selection.start = before.length + option.label.length + 1;\n        this.composer.selection.end = before.length + option.label.length + 1;\n        this.composer.forceCursorMove = true;\n    }\n    update() {\n        if (!this.search.delimiter) {\n            return;\n        }\n        const { type, suggestions } = this.suggestionService.searchSuggestions(this.search, {\n            thread: this.thread,\n            sort: true,\n        });\n        if (!suggestions.length) {\n            this.state.items = undefined;\n            return;\n        }\n        // arbitrary limit to avoid displaying too many elements at once\n        // ideally a load more mechanism should be introduced\n        const limit = 8;\n        suggestions.length = Math.min(suggestions.length, limit);\n        this.state.items = { type, suggestions };\n    }\n}\n\nexport function useSuggestion() {\n    return new UseSuggestion(useComponent());\n}\n", "import { partnerCompareRegistry } from \"@mail/core/common/partner_compare\";\nimport { cleanTerm } from \"@mail/utils/common/format\";\nimport { toRaw } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\n\nexport class SuggestionService {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    constructor(env, services) {\n        this.env = env;\n        this.orm = services.orm;\n        this.store = services[\"mail.store\"];\n    }\n\n    getSupportedDelimiters(thread) {\n        return [[\"@\"], [\"#\"], [\":\"]];\n    }\n\n    async fetchSuggestions({ delimiter, term }, { thread } = {}) {\n        const cleanedSearchTerm = cleanTerm(term);\n        switch (delimiter) {\n            case \"@\": {\n                await this.fetchPartners(cleanedSearchTerm, thread);\n                break;\n            }\n            case \"#\":\n                await this.fetchThreads(cleanedSearchTerm);\n                break;\n            case \":\":\n                await this.store.cannedReponses.fetch();\n                break;\n        }\n    }\n\n    /**\n     * @param {string} term\n     * @param {import(\"models\").Thread} [thread]\n     */\n    async fetchPartners(term, thread) {\n        const kwargs = { search: term };\n        if (thread?.model === \"discuss.channel\") {\n            kwargs.channel_id = thread.id;\n        }\n        const data = await this.orm.silent.call(\n            \"res.partner\",\n            thread?.model === \"discuss.channel\"\n                ? \"get_mention_suggestions_from_channel\"\n                : \"get_mention_suggestions\",\n            [],\n            kwargs\n        );\n        this.store.insert(data);\n    }\n\n    /**\n     * @param {string} term\n     */\n    async fetchThreads(term) {\n        const suggestedThreads = await this.orm.silent.call(\n            \"discuss.channel\",\n            \"get_mention_suggestions\",\n            [],\n            { search: term }\n        );\n        this.store.Thread.insert(suggestedThreads);\n    }\n\n    searchCannedResponseSuggestions(cleanedSearchTerm, sort) {\n        const cannedResponses = Object.values(this.store[\"mail.canned.response\"].records).filter(\n            (cannedResponse) => {\n                return cleanTerm(cannedResponse.source).includes(cleanedSearchTerm);\n            }\n        );\n        const sortFunc = (c1, c2) => {\n            const cleanedName1 = cleanTerm(c1.source);\n            const cleanedName2 = cleanTerm(c2.source);\n            if (\n                cleanedName1.startsWith(cleanedSearchTerm) &&\n                !cleanedName2.startsWith(cleanedSearchTerm)\n            ) {\n                return -1;\n            }\n            if (\n                !cleanedName1.startsWith(cleanedSearchTerm) &&\n                cleanedName2.startsWith(cleanedSearchTerm)\n            ) {\n                return 1;\n            }\n            if (cleanedName1 < cleanedName2) {\n                return -1;\n            }\n            if (cleanedName1 > cleanedName2) {\n                return 1;\n            }\n            return c1.id - c2.id;\n        };\n        return {\n            type: \"mail.canned.response\",\n            suggestions: sort ? cannedResponses.sort(sortFunc) : cannedResponses,\n        };\n    }\n\n    /**\n     * Returns suggestions that match the given search term from specified type.\n     *\n     * @param {Object} [param0={}]\n     * @param {String} [param0.delimiter] can be one one of the following: [\"@\", \"#\"]\n     * @param {String} [param0.term]\n     * @param {Object} [options={}]\n     * @param {Integer} [options.thread] prioritize and/or restrict\n     *  result in the context of given thread\n     * @returns {{ type: String, suggestions: Array }}\n     */\n    searchSuggestions({ delimiter, term }, { thread, sort = false } = {}) {\n        thread = toRaw(thread);\n        const cleanedSearchTerm = cleanTerm(term);\n        switch (delimiter) {\n            case \"@\": {\n                return this.searchPartnerSuggestions(cleanedSearchTerm, thread, sort);\n            }\n            case \"#\":\n                return this.searchChannelSuggestions(cleanedSearchTerm, sort);\n            case \":\":\n                return this.searchCannedResponseSuggestions(cleanedSearchTerm, sort);\n        }\n        return {\n            type: undefined,\n            suggestions: [],\n        };\n    }\n\n    getPartnerSuggestions(thread) {\n        let partners;\n        const isNonPublicChannel =\n            thread &&\n            (thread.channel_type === \"group\" ||\n                thread.channel_type === \"chat\" ||\n                (thread.channel_type === \"channel\" && thread.authorizedGroupFullName));\n        if (isNonPublicChannel) {\n            // Only return the channel members when in the context of a\n            // group restricted channel. Indeed, the message with the mention\n            // would be notified to the mentioned partner, so this prevents\n            // from inadvertently leaking the private message to the\n            // mentioned partner.\n            partners = thread.channelMembers\n                .map((member) => member.persona)\n                .filter((persona) => persona.type === \"partner\");\n        } else {\n            partners = Object.values(this.store.Persona.records).filter((persona) => {\n                if (thread?.model !== \"discuss.channel\" && persona.eq(this.store.odoobot)) {\n                    return false;\n                }\n                return persona.type === \"partner\";\n            });\n        }\n        return partners;\n    }\n\n    searchPartnerSuggestions(cleanedSearchTerm, thread, sort) {\n        const partners = this.getPartnerSuggestions(thread);\n        const suggestions = [];\n        for (const partner of partners) {\n            if (!partner.name) {\n                continue;\n            }\n            if (\n                cleanTerm(partner.name).includes(cleanedSearchTerm) ||\n                (partner.email && cleanTerm(partner.email).includes(cleanedSearchTerm))\n            ) {\n                suggestions.push(partner);\n            }\n        }\n        suggestions.push(\n            ...this.store.specialMentions.filter(\n                (special) =>\n                    thread &&\n                    special.channel_types.includes(thread.channel_type) &&\n                    cleanedSearchTerm.length >= Math.min(4, special.label.length) &&\n                    (special.label.startsWith(cleanedSearchTerm) ||\n                        cleanTerm(special.description.toString()).includes(cleanedSearchTerm))\n            )\n        );\n        return {\n            type: \"Partner\",\n            suggestions: sort\n                ? [...this.sortPartnerSuggestions(suggestions, cleanedSearchTerm, thread)]\n                : suggestions,\n        };\n    }\n\n    /**\n     * @param {[import(\"models\").Persona | import(\"@mail/core/common/store_service\").SpecialMention]} [partners]\n     * @param {String} [searchTerm]\n     * @param {import(\"models\").Thread} thread\n     * @returns {[import(\"models\").Persona]}\n     */\n    sortPartnerSuggestions(partners, searchTerm = \"\", thread = undefined) {\n        const cleanedSearchTerm = cleanTerm(searchTerm);\n        const compareFunctions = partnerCompareRegistry.getAll();\n        const context = { recentChatPartnerIds: this.store.getRecentChatPartnerIds() };\n        const memberPartnerIds = new Set(\n            thread?.channelMembers\n                .filter((member) => member.persona.type === \"partner\")\n                .map((member) => member.persona.id)\n        );\n        return partners.sort((p1, p2) => {\n            p1 = toRaw(p1);\n            p2 = toRaw(p2);\n            if (p1.isSpecial || p2.isSpecial) {\n                return 0;\n            }\n            for (const fn of compareFunctions) {\n                const result = fn(p1, p2, {\n                    env: this.env,\n                    memberPartnerIds,\n                    searchTerms: cleanedSearchTerm,\n                    thread,\n                    context,\n                });\n                if (result !== undefined) {\n                    return result;\n                }\n            }\n        });\n    }\n\n    searchChannelSuggestions(cleanedSearchTerm, sort) {\n        const suggestionList = Object.values(this.store.Thread.records).filter(\n            (thread) =>\n                thread.channel_type === \"channel\" &&\n                thread.displayName &&\n                cleanTerm(thread.displayName).includes(cleanedSearchTerm)\n        );\n        const sortFunc = (c1, c2) => {\n            const isPublicChannel1 = c1.channel_type === \"channel\" && !c2.authorizedGroupFullName;\n            const isPublicChannel2 = c2.channel_type === \"channel\" && !c2.authorizedGroupFullName;\n            if (isPublicChannel1 && !isPublicChannel2) {\n                return -1;\n            }\n            if (!isPublicChannel1 && isPublicChannel2) {\n                return 1;\n            }\n            if (c1.hasSelfAsMember && !c2.hasSelfAsMember) {\n                return -1;\n            }\n            if (!c1.hasSelfAsMember && c2.hasSelfAsMember) {\n                return 1;\n            }\n            const cleanedDisplayName1 = cleanTerm(c1.displayName);\n            const cleanedDisplayName2 = cleanTerm(c2.displayName);\n            if (\n                cleanedDisplayName1.startsWith(cleanedSearchTerm) &&\n                !cleanedDisplayName2.startsWith(cleanedSearchTerm)\n            ) {\n                return -1;\n            }\n            if (\n                !cleanedDisplayName1.startsWith(cleanedSearchTerm) &&\n                cleanedDisplayName2.startsWith(cleanedSearchTerm)\n            ) {\n                return 1;\n            }\n            if (cleanedDisplayName1 < cleanedDisplayName2) {\n                return -1;\n            }\n            if (cleanedDisplayName1 > cleanedDisplayName2) {\n                return 1;\n            }\n            return c1.id - c2.id;\n        };\n        return {\n            type: \"Thread\",\n            suggestions: sort ? suggestionList.sort(sortFunc) : suggestionList,\n        };\n    }\n}\n\nexport const suggestionService = {\n    dependencies: [\"orm\", \"mail.store\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    start(env, services) {\n        return new SuggestionService(env, services);\n    },\n};\n\nregistry.category(\"services\").add(\"mail.suggestion\", suggestionService);\n", "import { DateSection } from \"@mail/core/common/date_section\";\nimport { Message } from \"@mail/core/common/message\";\nimport { Record } from \"@mail/core/common/record\";\nimport { useVisible } from \"@mail/utils/common/hooks\";\n\nimport {\n    Component,\n    markRaw,\n    onMounted,\n    onWillDestroy,\n    onWillPatch,\n    onWillUpdateProps,\n    reactive,\n    toRaw,\n    useChildSubEnv,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Transition } from \"@web/core/transition\";\nimport { useBus, useRefListener, useService } from \"@web/core/utils/hooks\";\nimport { escape } from \"@web/core/utils/strings\";\n\nexport const PRESENT_VIEWPORT_THRESHOLD = 3;\nconst PRESENT_MESSAGE_THRESHOLD = 10;\n\n/**\n * @typedef {Object} Props\n * @property {boolean} [isInChatWindow=false]\n * @property {number} [jumpPresent=0]\n * @property {import(\"@mail/utils/common/hooks\").MessageEdition} [messageEdition]\n * @property {import(\"@mail/utils/common/hooks\").MessageToReplyTo} [messageToReplyTo]\n * @property {\"asc\"|\"desc\"} [order=\"asc\"]\n * @property {import(\"models\").Thread} thread\n * @property {string} [searchTerm]\n * @property {import(\"@web/core/utils/hooks\").Ref} [scrollRef]\n * @extends {Component<Props, Env>}\n */\nexport class Thread extends Component {\n    static components = { Message, Transition, DateSection };\n    static props = [\n        \"showDates?\",\n        \"isInChatWindow?\",\n        \"jumpPresent?\",\n        \"thread\",\n        \"messageEdition?\",\n        \"messageToReplyTo?\",\n        \"order?\",\n        \"scrollRef?\",\n        \"showEmptyMessage?\",\n        \"showJumpPresent?\",\n        \"messageActions?\",\n    ];\n    static defaultProps = {\n        isInChatWindow: false,\n        jumpPresent: 0,\n        order: \"asc\",\n        showDates: true,\n        showEmptyMessage: true,\n        showJumpPresent: true,\n        messageActions: true,\n    };\n    static template = \"mail.Thread\";\n\n    setup() {\n        super.setup();\n        this.escape = escape;\n        this.registerMessageRef = this.registerMessageRef.bind(this);\n        this.store = useState(useService(\"mail.store\"));\n        this.state = useState({\n            isReplyingTo: false,\n            mountedAndLoaded: false,\n            showJumpPresent: false,\n            scrollTop: null,\n        });\n        this.lastJumpPresent = this.props.jumpPresent;\n        this.orm = useService(\"orm\");\n        /** @type {ReturnType<import('@mail/utils/common/hooks').useMessageHighlight>|null} */\n        this.messageHighlight = this.env.messageHighlight\n            ? useState(this.env.messageHighlight)\n            : null;\n        this.scrollingToHighlight = false;\n        this.refByMessageId = reactive(new Map(), () => this.scrollToHighlighted());\n        useEffect(\n            () => this.scrollToHighlighted(),\n            () => [this.messageHighlight?.highlightedMessageId]\n        );\n        this.present = useRef(\"load-newer\");\n        this.jumpPresentRef = useRef(\"jump-present\");\n        this.root = useRef(\"messages\");\n        /**\n         * This is the reference element with the scrollbar. The reference can\n         * either be the chatter scrollable (if chatter) or the thread\n         * scrollable (in other cases).\n         */\n        this.scrollableRef = this.props.scrollRef ?? this.root;\n        useRefListener(\n            this.scrollableRef,\n            \"scrollend\",\n            () => (this.state.scrollTop = this.scrollableRef.el.scrollTop)\n        );\n        useEffect(\n            (loadNewer, mountedAndLoaded, unreadSynced) => {\n                if (\n                    loadNewer ||\n                    unreadSynced || // just marked as unread (local and server state are synced)\n                    !mountedAndLoaded ||\n                    !this.props.thread.selfMember ||\n                    !this.scrollableRef.el\n                ) {\n                    return;\n                }\n                const el = this.scrollableRef.el;\n                if (Math.abs(el.scrollTop + el.clientHeight - el.scrollHeight) <= 1) {\n                    this.props.thread.selfMember.hideUnreadBanner = true;\n                }\n            },\n            () => [\n                this.props.thread.loadNewer,\n                this.state.mountedAndLoaded,\n                this.props.thread.selfMember?.unreadSynced,\n                this.state.scrollTop,\n            ]\n        );\n        this.loadOlderState = useVisible(\n            \"load-older\",\n            async () => {\n                await this.messageHighlight?.scrollPromise;\n                if (this.loadOlderState.isVisible) {\n                    toRaw(this.props.thread).fetchMoreMessages();\n                }\n            },\n            { ready: false }\n        );\n        this.loadNewerState = useVisible(\n            \"load-newer\",\n            async () => {\n                await this.messageHighlight?.scrollPromise;\n                if (this.loadNewerState.isVisible) {\n                    toRaw(this.props.thread).fetchMoreMessages(\"newer\");\n                }\n            },\n            { ready: false }\n        );\n        this.presentThresholdState = useVisible(\"present-treshold\", () =>\n            this.updateShowJumpPresent()\n        );\n        this.setupScroll();\n        useEffect(\n            () => {\n                if (!this.viewportEl || !this.jumpPresentRef.el) {\n                    return;\n                }\n                const width = this.viewportEl.clientWidth;\n                const height = this.viewportEl.clientHeight;\n                const computedStyle = window.getComputedStyle(this.viewportEl);\n                const ps = parseInt(computedStyle.getPropertyValue(\"padding-left\"));\n                const pe = parseInt(computedStyle.getPropertyValue(\"padding-right\"));\n                const pt = parseInt(computedStyle.getPropertyValue(\"padding-top\"));\n                const pb = parseInt(computedStyle.getPropertyValue(\"padding-bottom\"));\n                this.jumpPresentRef.el.style.transform = `translate(${\n                    this.env.inChatter ? 22 : width - ps - pe - 22\n                }px, ${\n                    this.env.inChatter && !this.env.inChatter.aside\n                        ? 0\n                        : height - pt - pb - (this.env.inChatter?.aside ? 75 : 0)\n                }px)`;\n            },\n            () => [this.jumpPresentRef.el, this.viewportEl]\n        );\n        useEffect(\n            () => this.updateShowJumpPresent(),\n            () => [this.props.thread.loadNewer]\n        );\n        useEffect(\n            () => {\n                if (this.props.jumpPresent !== this.lastJumpPresent) {\n                    this.messageHighlight?.clearHighlight();\n                    if (this.props.thread.loadNewer) {\n                        this.jumpToPresent();\n                    } else {\n                        if (this.props.order === \"desc\") {\n                            this.scrollableRef.el.scrollTop = 0;\n                        } else {\n                            this.scrollableRef.el.scrollTop =\n                                this.scrollableRef.el.scrollHeight -\n                                this.scrollableRef.el.clientHeight;\n                        }\n                        this.props.thread.scrollTop = \"bottom\";\n                    }\n                    this.lastJumpPresent = this.props.jumpPresent;\n                }\n            },\n            () => [this.props.jumpPresent]\n        );\n        useEffect(\n            () => {\n                if (this.props.thread.highlightMessage && this.state.mountedAndLoaded) {\n                    this.messageHighlight?.highlightMessage(\n                        this.props.thread.highlightMessage,\n                        this.props.thread\n                    );\n                    this.props.thread.highlightMessage = null;\n                }\n            },\n            () => [this.props.thread.highlightMessage, this.state.mountedAndLoaded]\n        );\n        useEffect(\n            () => {\n                if (!this.state.mountedAndLoaded) {\n                    return;\n                }\n                this.updateShowJumpPresent();\n            },\n            () => [this.state.mountedAndLoaded]\n        );\n        onMounted(() => {\n            if (!this.env.chatter || this.env.chatter?.fetchMessages) {\n                if (this.env.chatter) {\n                    this.env.chatter.fetchMessages = false;\n                }\n                if (this.props.thread.selfMember && this.props.thread.scrollUnread) {\n                    toRaw(this.props.thread).loadAround(\n                        this.props.thread.selfMember.new_message_separator\n                    );\n                } else {\n                    toRaw(this.props.thread).fetchNewMessages();\n                }\n            }\n        });\n        useEffect(\n            (isLoaded) => {\n                this.state.mountedAndLoaded = isLoaded;\n            },\n            /**\n             * Observe `mountedAndLoaded` as well because it might change from\n             * other parts of the code without `useEffect` detecting any change\n             * for `isLoaded`, and it should still be reset when patching.\n             */\n            () => [this.props.thread.isLoaded, this.state.mountedAndLoaded]\n        );\n        useBus(this.env.bus, \"MAIL:RELOAD-THREAD\", ({ detail }) => {\n            const { model, id } = this.props.thread;\n            if (detail.model === model && detail.id === id) {\n                toRaw(this.props.thread).fetchNewMessages();\n            }\n        });\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.thread.notEq(this.props.thread)) {\n                this.lastJumpPresent = nextProps.jumpPresent;\n            }\n            if (!this.env.chatter || this.env.chatter?.fetchMessages) {\n                if (this.env.chatter) {\n                    this.env.chatter.fetchMessages = false;\n                }\n                toRaw(nextProps.thread).fetchNewMessages();\n            }\n        });\n    }\n\n    /**\n     * The scroll on a message list is managed in several different ways.\n     *\n     * 1. When the user first accesses a thread with unread messages, or when\n     *    the user goes back to a thread with new unread messages, it should\n     *    scroll to the position of the first unread message if there is one.\n     * 2. When loading older or newer messages, the messages already on screen\n     *    should visually stay in place. When the extra messages are added at\n     *    the bottom (chatter loading older, or channel loading newer) the same\n     *    scroll top position should be kept, and when the extra messages are\n     *    added at the top (chatter loading newer, or channel loading older),\n     *    the extra height from the extra messages should be compensated in the\n     *    scroll position.\n     * 3. When the scroll is at the bottom, it should stay at the bottom when\n     *    there is a change of height: new messages, images loaded, ...\n     * 4. When the user goes back and forth between threads, it should restore\n     *    the last scroll position of each thread.\n     * 5. When currently highlighting a message it takes priority to allow the\n     *    highlighted message to be scrolled to.\n     */\n    setupScroll() {\n        const ref = this.scrollableRef;\n        /**\n         * Last scroll value that was automatically set. This prevents from\n         * setting the same value 2 times in a row. This is not supposed to have\n         * an effect, unless the value was changed from outside in the meantime,\n         * in which case resetting the value would incorrectly override the\n         * other change. This should give enough time to scroll/resize event to\n         * register the new scroll value.\n         */\n        let lastSetValue;\n        /**\n         * The snapshot mechanism (point 2) should only apply after the messages\n         * have been loaded and displayed at least once. Technically this is\n         * after the first patch following when `mountedAndLoaded` is true. This\n         * is what this variable holds.\n         */\n        let loadedAndPatched = false;\n        /**\n         * The snapshot of current scrollTop and scrollHeight for the purpose\n         * of keeping messages in place when loading older/newer (point 2).\n         */\n        let snapshot;\n        /**\n         * The newest message that is already rendered, useful to detect\n         * whether newer messages have been loaded since last render to decide\n         * when to apply the snapshot to keep messages in place (point 2).\n         */\n        let newestPersistentMessage;\n        /**\n         * The oldest message that is already rendered, useful to detect\n         * whether older messages have been loaded since last render to decide\n         * when to apply the snapshot to keep messages in place (point 2).\n         */\n        let oldestPersistentMessage;\n        /**\n         * Whether it was possible to load newer messages in the last rendered\n         * state, useful to decide when to apply the snapshot to keep messages\n         * in place (point 2).\n         */\n        let loadNewer;\n        const reset = () => {\n            this.state.mountedAndLoaded = false;\n            this.loadOlderState.ready = false;\n            this.loadNewerState.ready = false;\n            lastSetValue = undefined;\n            snapshot = undefined;\n            newestPersistentMessage = undefined;\n            oldestPersistentMessage = undefined;\n            loadedAndPatched = false;\n            loadNewer = false;\n        };\n        /**\n         * These states need to be immediately reset when the value changes on\n         * the record, because the transition is important, not only the final\n         * value. If resetting is depending on the update cycle, it can happen\n         * that the value quickly changes and then back again before there is\n         * any mounting/patching, and the change would therefore be undetected.\n         */\n        let stopOnChange = Record.onChange(this.props.thread, \"isLoaded\", () => {\n            if (!this.props.thread.isLoaded || !this.state.mountedAndLoaded) {\n                reset();\n            }\n        });\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.thread.notEq(this.props.thread)) {\n                stopOnChange();\n                stopOnChange = Record.onChange(nextProps.thread, \"isLoaded\", () => {\n                    if (!nextProps.thread.isLoaded || !this.state.mountedAndLoaded) {\n                        reset();\n                    }\n                });\n            }\n        });\n        onWillDestroy(() => stopOnChange());\n        const saveScroll = () => {\n            const thread = toRaw(this.props.thread);\n            const isBottom =\n                this.props.order === \"asc\"\n                    ? ref.el.scrollHeight - ref.el.scrollTop - ref.el.clientHeight < 30\n                    : ref.el.scrollTop < 30;\n            if (isBottom) {\n                thread.scrollTop = \"bottom\";\n            } else {\n                thread.scrollTop =\n                    this.props.order === \"asc\"\n                        ? ref.el.scrollTop\n                        : ref.el.scrollHeight - ref.el.scrollTop - ref.el.clientHeight;\n            }\n        };\n        const setScroll = (value) => {\n            ref.el.scrollTop = value;\n            lastSetValue = value;\n            saveScroll();\n        };\n        const applyScroll = () => {\n            if (!this.props.thread.isLoaded || !this.state.mountedAndLoaded) {\n                reset();\n                return;\n            }\n            // Use toRaw() to prevent scroll check from triggering renders.\n            const thread = toRaw(this.props.thread);\n            const olderMessages = thread.oldestPersistentMessage?.id < oldestPersistentMessage?.id;\n            const newerMessages = thread.newestPersistentMessage?.id > newestPersistentMessage?.id;\n            const messagesAtTop =\n                (this.props.order === \"asc\" && olderMessages) ||\n                (this.props.order === \"desc\" && newerMessages);\n            const messagesAtBottom =\n                (this.props.order === \"desc\" && olderMessages) ||\n                (this.props.order === \"asc\" &&\n                    newerMessages &&\n                    (loadNewer || thread.scrollTop !== \"bottom\"));\n            if (thread.selfMember && thread.scrollUnread) {\n                if (thread.firstUnreadMessage) {\n                    const messageEl = this.refByMessageId.get(thread.firstUnreadMessage.id)?.el;\n                    if (!messageEl) {\n                        return;\n                    }\n                    const messageCenter =\n                        messageEl.offsetTop -\n                        this.scrollableRef.el.offsetHeight / 2 +\n                        messageEl.offsetHeight / 2;\n                    setScroll(messageCenter);\n                } else {\n                    const scrollTop =\n                        this.props.order === \"asc\"\n                            ? this.scrollableRef.el.scrollHeight -\n                              this.scrollableRef.el.clientHeight\n                            : 0;\n                    setScroll(scrollTop);\n                }\n                thread.scrollUnread = false;\n            } else if (snapshot && messagesAtTop) {\n                setScroll(snapshot.scrollTop + ref.el.scrollHeight - snapshot.scrollHeight);\n            } else if (snapshot && messagesAtBottom) {\n                setScroll(snapshot.scrollTop);\n            } else if (\n                !this.env.messageHighlight?.highlightedMessageId &&\n                thread.scrollTop !== undefined\n            ) {\n                let value;\n                if (thread.scrollTop === \"bottom\") {\n                    value =\n                        this.props.order === \"asc\" ? ref.el.scrollHeight - ref.el.clientHeight : 0;\n                } else {\n                    value =\n                        this.props.order === \"asc\"\n                            ? thread.scrollTop\n                            : ref.el.scrollHeight - thread.scrollTop - ref.el.clientHeight;\n                }\n                if (lastSetValue === undefined || Math.abs(lastSetValue - value) > 1) {\n                    setScroll(value);\n                }\n            }\n            snapshot = undefined;\n            newestPersistentMessage = thread.newestPersistentMessage;\n            oldestPersistentMessage = thread.oldestPersistentMessage;\n            loadNewer = thread.loadNewer;\n            if (!loadedAndPatched) {\n                loadedAndPatched = true;\n                this.loadOlderState.ready = true;\n                this.loadNewerState.ready = true;\n            }\n        };\n        onWillPatch(() => {\n            if (!loadedAndPatched) {\n                return;\n            }\n            snapshot = {\n                scrollHeight: ref.el.scrollHeight,\n                scrollTop: ref.el.scrollTop,\n            };\n        });\n        useEffect(applyScroll);\n        useChildSubEnv({\n            onImageLoaded: applyScroll,\n        });\n        const observer = new ResizeObserver(applyScroll);\n        useEffect(\n            (el, mountedAndLoaded) => {\n                if (el && mountedAndLoaded) {\n                    el.addEventListener(\"scroll\", saveScroll);\n                    observer.observe(el);\n                    return () => {\n                        observer.unobserve(el);\n                        el.removeEventListener(\"scroll\", saveScroll);\n                    };\n                }\n            },\n            () => [ref.el, this.state.mountedAndLoaded]\n        );\n    }\n\n    get viewportEl() {\n        let viewportEl = this.scrollableRef.el;\n        if (viewportEl && viewportEl.clientHeight > browser.innerHeight) {\n            while (viewportEl && viewportEl.clientHeight > browser.innerHeight) {\n                viewportEl = viewportEl.parentElement;\n            }\n        }\n        return viewportEl;\n    }\n\n    get PRESENT_THRESHOLD() {\n        const viewportHeight = (this.getViewportEl?.clientHeight ?? 0) * PRESENT_VIEWPORT_THRESHOLD;\n        const messagesHeight = [...this.props.thread.nonEmptyMessages]\n            .reverse()\n            .slice(0, PRESENT_MESSAGE_THRESHOLD)\n            .map((message) => this.refByMessageId.get(message.id))\n            .reduce((totalHeight, message) => totalHeight + (message?.el?.clientHeight ?? 0), 0);\n        const threshold = Math.max(viewportHeight, messagesHeight);\n        return this.state.showJumpPresent ? threshold - 200 : threshold;\n    }\n\n    get newMessageBannerText() {\n        if (this.props.thread.selfMember?.totalUnreadMessageCounter > 1) {\n            return _t(\"%s new messages\", this.props.thread.selfMember.totalUnreadMessageCounter);\n        }\n        return _t(\"1 new message\");\n    }\n\n    get preferenceButtonText() {\n        const [, before, inside, after] =\n            _t(\n                \"<button>Change your preferences</button> to receive new notifications in your inbox.\"\n            ).match(/(.*)<button>(.*)<\\/button>(.*)/) ?? [];\n        return { before, inside, after };\n    }\n\n    updateShowJumpPresent() {\n        this.state.showJumpPresent =\n            this.props.thread.loadNewer || this.presentThresholdState.isVisible === false;\n    }\n\n    onClickLoadOlder() {\n        this.props.thread.fetchMoreMessages();\n    }\n\n    async onClickPreferences() {\n        const actionDescription = await this.orm.call(\"res.users\", \"action_get\");\n        actionDescription.res_id = this.store.self.userId;\n        this.env.services.action.doAction(actionDescription);\n    }\n\n    getMessageClassName(message) {\n        return !message.isNotification && this.messageHighlight?.highlightedMessageId === message.id\n            ? \"o-highlighted bg-view shadow-lg pb-1\"\n            : \"\";\n    }\n\n    async jumpToPresent() {\n        this.messageHighlight?.clearHighlight();\n        await this.props.thread.loadAround();\n        this.props.thread.loadNewer = false;\n        this.props.thread.scrollTop = \"bottom\";\n        this.state.showJumpPresent = false;\n    }\n\n    async onClickUnreadMessagesBanner() {\n        await this.props.thread.loadAround(this.props.thread.selfMember.localNewMessageSeparator);\n        this.messageHighlight?.highlightMessage(\n            this.props.thread.firstUnreadMessage,\n            this.props.thread\n        );\n    }\n\n    registerMessageRef(message, ref) {\n        if (!ref) {\n            this.refByMessageId.delete(message.id);\n            return;\n        }\n        this.refByMessageId.set(message.id, markRaw(ref));\n    }\n\n    isSquashed(msg, prevMsg) {\n        if (this.props.thread.model === \"mail.box\") {\n            return false;\n        }\n        if (\n            !prevMsg ||\n            prevMsg.message_type === \"notification\" ||\n            prevMsg.isEmpty ||\n            this.env.inChatter\n        ) {\n            return false;\n        }\n\n        if (!msg.author?.eq(prevMsg.author)) {\n            return false;\n        }\n        if (!msg.thread?.eq(prevMsg.thread)) {\n            return false;\n        }\n        return msg.datetime.ts - prevMsg.datetime.ts < 5 * 60 * 1000;\n    }\n\n    scrollToHighlighted() {\n        if (!this.messageHighlight?.highlightedMessageId || this.scrollingToHighlight) {\n            return;\n        }\n        const el = this.refByMessageId.get(this.messageHighlight.highlightedMessageId)?.el;\n        if (el) {\n            this.scrollingToHighlight = true;\n            this.messageHighlight.scrollTo(el).then(() => (this.scrollingToHighlight = false));\n        }\n    }\n\n    get orderedMessages() {\n        return this.props.order === \"asc\"\n            ? [...this.props.thread.nonEmptyMessages]\n            : [...this.props.thread.nonEmptyMessages].reverse();\n    }\n}\n", "import { useSubEnv, useComponent, useState } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { SearchMessagesPanel } from \"@mail/core/common/search_messages_panel\";\n\nexport const threadActionsRegistry = registry.category(\"mail.thread/actions\");\n\nthreadActionsRegistry\n    .add(\"fold-chat-window\", {\n        condition(component) {\n            return (\n                !component.ui.isSmall &&\n                component.props.chatWindow &&\n                component.props.chatWindow.thread\n            );\n        },\n        icon: \"fa fa-fw fa-minus\",\n        name(component) {\n            return !component.props.chatWindow?.isOpen ? _t(\"Open\") : _t(\"Fold\");\n        },\n        open(component) {\n            component.toggleFold();\n        },\n        displayActive(component) {\n            return !component.props.chatWindow?.isOpen;\n        },\n        sequence: 99,\n        sequenceQuick: 20,\n    })\n    .add(\"rename-thread\", {\n        condition(component) {\n            return (\n                component.thread &&\n                component.props.chatWindow?.isOpen &&\n                (component.thread.is_editable || component.thread.channel_type === \"chat\")\n            );\n        },\n        icon: \"fa fa-fw fa-pencil\",\n        name: _t(\"Rename Thread\"),\n        open(component) {\n            component.state.editingName = true;\n        },\n        sequence: 30,\n        sequenceGroup: 20,\n    })\n    .add(\"close\", {\n        condition(component) {\n            return component.props.chatWindow;\n        },\n        icon: \"oi fa-fw oi-close\",\n        name: _t(\"Close Chat Window (ESC)\"),\n        open(component) {\n            component.close();\n        },\n        sequence: 100,\n        sequenceQuick: 10,\n    })\n    .add(\"search-messages\", {\n        component: SearchMessagesPanel,\n        condition(component) {\n            return (\n                [\"discuss.channel\", \"mail.box\"].includes(component.thread?.model) &&\n                (!component.props.chatWindow || component.props.chatWindow.isOpen)\n            );\n        },\n        panelOuterClass: \"o-mail-SearchMessagesPanel bg-inherit\",\n        icon: \"oi oi-fw oi-search\",\n        iconLarge: \"oi oi-fw fa-lg oi-search\",\n        name: _t(\"Search Messages\"),\n        nameActive: _t(\"Close Search\"),\n        sequence: 20,\n        sequenceGroup: 20,\n        setup(action) {\n            useSubEnv({\n                searchMenu: {\n                    open: () => action.open(),\n                    close: () => {\n                        if (action.isActive) {\n                            action.close();\n                        }\n                    },\n                },\n            });\n        },\n        toggle: true,\n    });\n\nfunction transformAction(component, id, action) {\n    return {\n        /** Closes this action. */\n        close() {\n            if (this.toggle) {\n                component.threadActions.activeAction = component.threadActions.actionStack.pop();\n            }\n            action.close?.(component, this);\n        },\n        /** Optional component that should be displayed in the view when this action is active. */\n        component: action.component,\n        /** Condition to display the component of this action. */\n        get componentCondition() {\n            return this.isActive && this.component && this.condition && !this.popover;\n        },\n        /** Props to pass to the component of this action. */\n        get componentProps() {\n            return action.componentProps?.(this, component);\n        },\n        /** Condition to display this action. */\n        get condition() {\n            return action.condition(component);\n        },\n        /** Condition to disable the button of this action (but still display it). */\n        get disabledCondition() {\n            return action.disabledCondition?.(component);\n        },\n        /** Icon for the button this action. */\n        get icon() {\n            return typeof action.icon === \"function\" ? action.icon(component) : action.icon;\n        },\n        /** Large icon for the button this action. */\n        get iconLarge() {\n            return typeof action.iconLarge === \"function\"\n                ? action.iconLarge(component)\n                : action.iconLarge ?? action.icon;\n        },\n        /** Unique id of this action. */\n        id,\n        /** States whether this action is currently active. */\n        get isActive() {\n            return id === component.threadActions.activeAction?.id;\n        },\n        /** Name of this action, displayed to the user. */\n        get name() {\n            const res = this.isActive && action.nameActive ? action.nameActive : action.name;\n            return typeof res === \"function\" ? res(component) : res;\n        },\n        /**\n         * Action to execute when this action is selected (on or off).\n         *\n         * @param {object} [param0]\n         * @param {boolean} [param0.keepPrevious] Whether the previous action\n         * should be kept so that closing the current action goes back\n         * to the previous one.\n         * */\n        onSelect({ keepPrevious } = {}) {\n            if (this.toggle && this.isActive) {\n                this.close();\n            } else {\n                this.open({ keepPrevious });\n            }\n        },\n        /**\n         * Opens this action.\n         *\n         * @param {object} [param0]\n         * @param {boolean} [param0.keepPrevious] Whether the previous action\n         * should be kept so that closing the current action goes back\n         * to the previous one.\n         * */\n        open({ keepPrevious } = {}) {\n            if (this.toggle) {\n                if (component.threadActions.activeAction && keepPrevious) {\n                    component.threadActions.actionStack.push(component.threadActions.activeAction);\n                }\n                component.threadActions.activeAction = this;\n            }\n            action.open?.(component, this);\n        },\n        get panelOuterClass() {\n            return typeof action.panelOuterClass === \"function\"\n                ? action.panelOuterClass(component)\n                : action.panelOuterClass;\n        },\n        /** Determines whether this is a popover linked to this action. */\n        popover: null,\n        /** Determines the order of this action (smaller first). */\n        get sequence() {\n            return typeof action.sequence === \"function\"\n                ? action.sequence(component)\n                : action.sequence;\n        },\n        get sequenceGroup() {\n            return typeof action.sequenceGroup === \"function\"\n                ? action.sequenceGroup(component)\n                : action.sequenceGroup;\n        },\n        get sequenceQuick() {\n            return typeof action.sequenceQuick === \"function\"\n                ? action.sequenceQuick(component)\n                : action.sequenceQuick;\n        },\n        /** Component setup to execute when this action is registered. */\n        setup: action.setup,\n        /** Text for the button of this action */\n        text: action.text,\n        /** Determines whether this action is a one time effect or can be toggled (on or off). */\n        toggle: action.toggle,\n    };\n}\n\nexport function useThreadActions() {\n    const component = useComponent();\n    const transformedActions = threadActionsRegistry\n        .getEntries()\n        .map(([id, action]) => transformAction(component, id, action));\n    for (const action of transformedActions) {\n        if (action.setup) {\n            action.setup(action);\n        }\n    }\n    const state = useState({\n        get actions() {\n            return transformedActions\n                .filter((action) => action.condition)\n                .sort((a1, a2) => a1.sequence - a2.sequence);\n        },\n        get partition() {\n            const actions = transformedActions.filter((action) => action.condition);\n            const quick = actions\n                .filter((a) => a.sequenceQuick)\n                .sort((a1, a2) => a1.sequenceQuick - a2.sequenceQuick);\n            const grouped = actions.filter((a) => a.sequenceGroup);\n            const groups = {};\n            for (const a of grouped) {\n                if (!(a.sequenceGroup in groups)) {\n                    groups[a.sequenceGroup] = [];\n                }\n                groups[a.sequenceGroup].push(a);\n            }\n            const sortedGroups = Object.entries(groups).sort(\n                ([groupId1], [groupId2]) => groupId1 - groupId2\n            );\n            for (const [, actions] of sortedGroups) {\n                actions.sort((a1, a2) => a1.sequence - a2.sequence);\n            }\n            const group = sortedGroups.map(([groupId, actions]) => actions);\n            const other = actions\n                .filter((a) => !a.sequenceQuick & !a.sequenceGroup)\n                .sort((a1, a2) => a1.sequence - a2.sequence);\n            return { quick, group, other };\n        },\n        actionStack: [],\n        activeAction: null,\n    });\n    return state;\n}\n", "import { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { Thread } from \"./thread_model\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Thread} thread\n * @property {string} size\n * @property {string} className\n * @extends {Component<Props, Env>}\n */\nexport class ThreadIcon extends Component {\n    static template = \"mail.ThreadIcon\";\n    static props = {\n        thread: { type: Thread },\n        size: { optional: true, validate: (size) => [\"small\", \"medium\", \"large\"].includes(size) },\n        className: { type: String, optional: true },\n    };\n    static defaultProps = {\n        size: \"medium\",\n        className: \"\",\n    };\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n    }\n\n    get correspondent() {\n        return this.props.thread.correspondent;\n    }\n\n    get defaultChatIcon() {\n        return {\n            class: \"fa fa-question-circle\",\n            title: _t(\"No IM status available\"),\n        };\n    }\n}\n", "import { AND, Record } from \"@mail/core/common/record\";\nimport { prettifyMessageContent } from \"@mail/utils/common/format\";\nimport { assignDefined, compareDatetime, nearestGreaterThanOrEqual } from \"@mail/utils/common/misc\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatList } from \"@web/core/l10n/utils\";\nimport { user } from \"@web/core/user\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\n\n/**\n * @typedef SuggestedRecipient\n * @property {string} email\n * @property {import(\"models\").Persona|false} persona\n * @property {string} lang\n * @property {string} reason\n * @property {boolean} checked\n */\n\nexport class Thread extends Record {\n    static id = AND(\"model\", \"id\");\n    /** @type {Object.<string, import(\"models\").Thread>} */\n    static records = {};\n    /** @returns {import(\"models\").Thread} */\n    static get(data) {\n        return super.get(data);\n    }\n    /**\n     * @param {string} localId\n     * @returns {string}\n     */\n    static localIdToActiveId(localId) {\n        if (!localId) {\n            return undefined;\n        }\n        // Transform \"Thread,<model> AND <id>\" to \"<model>_<id>\"\"\n        return localId.split(\",\").slice(1).join(\"_\").replace(\" AND \", \"_\");\n    }\n    /** @returns {import(\"models\").Thread|import(\"models\").Thread[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n    static new() {\n        const thread = super.new(...arguments);\n        Record.onChange(thread, [\"state\"], () => {\n            if (thread.state === \"open\" && !this.store.env.services.ui.isSmall) {\n                const cw = this.store.ChatWindow?.insert({ thread });\n                thread.store.chatHub.opened.delete(cw);\n                thread.store.chatHub.opened.unshift(cw);\n            }\n            if (thread.state === \"folded\") {\n                const cw = this.store.ChatWindow?.insert({ thread });\n                thread.store.chatHub.folded.delete(cw);\n                thread.store.chatHub.folded.unshift(cw);\n            }\n        });\n        return thread;\n    }\n    static async getOrFetch(data) {\n        return this.get(data);\n    }\n\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    uuid;\n    /** @type {string} */\n    model;\n    allMessages = Record.many(\"Message\", {\n        inverse: \"thread\",\n    });\n    /** @type {boolean} */\n    areAttachmentsLoaded = false;\n    attachments = Record.many(\"Attachment\", {\n        /**\n         * @param {import(\"models\").Attachment} a1\n         * @param {import(\"models\").Attachment} a2\n         */\n        sort: (a1, a2) => (a1.id < a2.id ? 1 : -1),\n    });\n    get canLeave() {\n        return (\n            [\"channel\", \"group\"].includes(this.channel_type) &&\n            !this.message_needaction_counter &&\n            !this.group_based_subscription &&\n            this.store.self?.type === \"partner\"\n        );\n    }\n    get canUnpin() {\n        return this.channel_type === \"chat\" && this.importantCounter === 0;\n    }\n    channelMembers = Record.many(\"ChannelMember\", {\n        inverse: \"thread\",\n        onDelete: (r) => r.delete(),\n        sort: (m1, m2) => m1.id - m2.id,\n    });\n    /**\n     * To be overridden.\n     * The purpose is to exclude technical channelMembers like bots and avoid\n     * \"wrong\" seen message indicator\n     */\n    get membersThatCanSeen() {\n        return this.channelMembers;\n    }\n    typingMembers = Record.many(\"ChannelMember\", { inverse: \"threadAsTyping\" });\n    otherTypingMembers = Record.many(\"ChannelMember\", {\n        /** @this {import(\"models\").Thread} */\n        compute() {\n            return this.typingMembers.filter((member) => !member.persona?.eq(this.store.self));\n        },\n    });\n    hasOtherMembersTyping = Record.attr(false, {\n        /** @this {import(\"models\").Thread} */\n        compute() {\n            return this.otherTypingMembers.length > 0;\n        },\n    });\n    toggleBusSubscription = Record.attr(false, {\n        compute() {\n            return (\n                this.model === \"discuss.channel\" &&\n                this.selfMember?.memberSince >= this.store.env.services.bus_service.startedAt\n            );\n        },\n        onUpdate() {\n            this.store.updateBusSubscription();\n        },\n    });\n    invitedMembers = Record.many(\"ChannelMember\");\n    composer = Record.one(\"Composer\", {\n        compute: () => ({}),\n        inverse: \"thread\",\n        onDelete: (r) => r.delete(),\n    });\n    correspondent = Record.one(\"ChannelMember\", {\n        compute() {\n            return this.computeCorrespondent();\n        },\n    });\n    correspondentCountry = Record.one(\"Country\", {\n        /** @this {import(\"models\").Thread} */\n        compute() {\n            return this.correspondent?.persona?.country ?? this.anonymous_country;\n        },\n    });\n    get showCorrespondentCountry() {\n        return (\n            this.channel_type === \"livechat\" &&\n            this.operator?.eq(this.store.self) &&\n            Boolean(this.correspondentCountry)\n        );\n    }\n    counter = 0;\n    counter_bus_id = 0;\n    /** @type {string} */\n    custom_channel_name;\n    /** @type {string} */\n    description;\n    displayToSelf = Record.attr(false, {\n        compute() {\n            return (\n                this.is_pinned ||\n                ([\"channel\", \"group\"].includes(this.channel_type) &&\n                    this.hasSelfAsMember &&\n                    !this.parent_channel_id)\n            );\n        },\n        onUpdate() {\n            this.onPinStateUpdated();\n        },\n    });\n    followers = Record.many(\"Follower\", {\n        /** @this {import(\"models\").Thread} */\n        onAdd(r) {\n            r.thread = this;\n        },\n        onDelete: (r) => r.delete(),\n    });\n    selfFollower = Record.one(\"Follower\", {\n        /** @this {import(\"models\").Thread} */\n        onAdd(r) {\n            r.thread = this;\n        },\n        onDelete: (r) => r.delete(),\n    });\n    /** @type {integer|undefined} */\n    followersCount;\n    loadOlder = false;\n    loadNewer = false;\n    get importantCounter() {\n        if (this.model === \"mail.box\") {\n            return this.counter;\n        }\n        if (this.isChatChannel && this.selfMember?.message_unread_counter) {\n            return this.selfMember.totalUnreadMessageCounter;\n        }\n        return this.message_needaction_counter;\n    }\n    isCorrespondentOdooBot = Record.attr(undefined, {\n        compute() {\n            return this.correspondent?.persona.eq(this.store.odoobot);\n        },\n    });\n    isDisplayed = Record.attr(false, {\n        compute() {\n            return this.computeIsDisplayed();\n        },\n        onUpdate() {\n            if (this.selfMember && !this.isDisplayed) {\n                this.selfMember.syncUnread = true;\n            }\n        },\n    });\n    isLoadingAttachments = false;\n    isLoadedDeferred = new Deferred();\n    isLoaded = Record.attr(false, {\n        /** @this {import(\"models\").Thread} */\n        onUpdate() {\n            if (this.isLoaded) {\n                this.isLoadedDeferred.resolve();\n            } else {\n                const def = this.isLoadedDeferred;\n                this.isLoadedDeferred = new Deferred();\n                this.isLoadedDeferred.then(() => def.resolve());\n            }\n        },\n    });\n    is_pinned = Record.attr(undefined, {\n        /** @this {import(\"models\").Thread} */\n        onUpdate() {\n            this.onPinStateUpdated();\n        },\n    });\n    mainAttachment = Record.one(\"Attachment\");\n    memberCount = 0;\n    message_needaction_counter = 0;\n    message_needaction_counter_bus_id = 0;\n    /**\n     * Contains continuous sequence of messages to show in message list.\n     * Messages are ordered from older to most recent.\n     * There should not be any hole in this list: there can be unknown\n     * messages before start and after end, but there should not be any\n     * unknown in-between messages.\n     *\n     * Content should be fetched and inserted in a controlled way.\n     */\n    messages = Record.many(\"Message\");\n    /** @type {string} */\n    modelName;\n    /** @type {string} */\n    module_icon;\n    /**\n     * Contains messages received from the bus that are not yet inserted in\n     * `messages` list. This is a temporary storage to ensure nothing is lost\n     * when fetching newer messages.\n     */\n    pendingNewMessages = Record.many(\"Message\");\n    needactionMessages = Record.many(\"Message\", {\n        inverse: \"threadAsNeedaction\",\n        sort: (message1, message2) => message1.id - message2.id,\n    });\n    /** @type {string} */\n    name;\n    selfMember = Record.one(\"ChannelMember\", {\n        inverse: \"threadAsSelf\",\n    });\n    /** @type {'open' | 'folded' | 'closed'} */\n    state;\n    status = \"new\";\n    /**\n     * Stored scoll position of thread from top in ASC order.\n     *\n     * @type {number|'bottom'}\n     */\n    scrollTop = \"bottom\";\n    transientMessages = Record.many(\"Message\");\n    /** @type {string} */\n    defaultDisplayMode;\n    scrollUnread = true;\n    suggestedRecipients = Record.attr([], {\n        onUpdate() {\n            for (const recipient of this.suggestedRecipients) {\n                if (recipient.checked === undefined) {\n                    recipient.checked = true;\n                }\n                recipient.persona = recipient.partner_id\n                    ? { type: \"partner\", id: recipient.partner_id }\n                    : false;\n            }\n        },\n    });\n    hasLoadingFailed = false;\n    canPostOnReadonly;\n    /** @type {luxon.DateTime} */\n    last_interest_dt = Record.attr(undefined, { type: \"datetime\" });\n    /** @type {luxon.DateTime} */\n    lastInterestDt = Record.attr(undefined, {\n        type: \"datetime\",\n        compute() {\n            const selfMemberLastInterestDt = this.selfMember?.last_interest_dt;\n            const lastInterestDt = this.last_interest_dt;\n            return compareDatetime(selfMemberLastInterestDt, lastInterestDt) > 0\n                ? selfMemberLastInterestDt\n                : lastInterestDt;\n        },\n    });\n    /** @type {Boolean} */\n    is_editable;\n    /**\n     * This field is used for channels only.\n     * false means using the custom_notifications from user settings.\n     *\n     * @type {false|\"all\"|\"mentions\"|\"no_notif\"}\n     */\n    custom_notifications = false;\n    /** @type {luxon.DateTime} */\n    mute_until_dt = Record.attr(undefined, { type: \"datetime\" });\n    /** @type {Boolean} */\n    isLocallyPinned = Record.attr(false, {\n        onUpdate() {\n            this.onPinStateUpdated();\n        },\n    });\n    /** @type {\"not_fetched\"|\"pending\"|\"fetched\"} */\n    fetchMembersState = \"not_fetched\";\n    /** @type {integer|null} */\n    highlightMessage = Record.one(\"Message\", {\n        onAdd(msg) {\n            msg.thread = this;\n        },\n    });\n    /** @type {String|undefined} */\n    access_token;\n    /** @type {String|undefined} */\n    hash;\n    /**\n     * Partner id for non channel threads\n     *  @type {integer|undefined}\n     */\n    pid;\n\n    get accessRestrictedToGroupText() {\n        if (!this.authorizedGroupFullName) {\n            return false;\n        }\n        return _t('Access restricted to group \"%(groupFullName)s\"', {\n            groupFullName: this.authorizedGroupFullName,\n        });\n    }\n\n    get areAllMembersLoaded() {\n        return this.memberCount === this.channelMembers.length;\n    }\n\n    get busChannel() {\n        return `${this.model}_${this.id}`;\n    }\n\n    get followersFullyLoaded() {\n        return (\n            this.followersCount ===\n            (this.selfFollower ? this.followers.length + 1 : this.followers.length)\n        );\n    }\n\n    get attachmentsInWebClientView() {\n        const attachments = this.attachments.filter(\n            (attachment) => (attachment.isPdf || attachment.isImage) && !attachment.uploading\n        );\n        attachments.sort((a1, a2) => {\n            return a2.id - a1.id;\n        });\n        return attachments;\n    }\n\n    get isUnread() {\n        return this.selfMember?.message_unread_counter > 0 || this.needactionMessages.length > 0;\n    }\n\n    get isMuted() {\n        return this.mute_until_dt || this.store.settings.mute_until_dt;\n    }\n\n    get typesAllowingCalls() {\n        return [\"chat\", \"channel\", \"group\"];\n    }\n\n    get allowCalls() {\n        return (\n            this.typesAllowingCalls.includes(this.channel_type) &&\n            !this.correspondent?.persona.eq(this.store.odoobot)\n        );\n    }\n\n    get hasAttachmentPanel() {\n        return this.model === \"discuss.channel\";\n    }\n\n    get isChatChannel() {\n        return [\"chat\", \"group\"].includes(this.channel_type);\n    }\n\n    get displayName() {\n        if (this.channel_type === \"chat\" && this.correspondent) {\n            return this.custom_channel_name || this.correspondent.persona.name;\n        }\n        if (this.channel_type === \"group\" && !this.name) {\n            return formatList(\n                this.channelMembers.map((channelMember) => channelMember.persona.name)\n            );\n        }\n        return this.name;\n    }\n\n    get correspondents() {\n        return this.channelMembers.filter(({ persona }) => persona.notEq(this.store.self));\n    }\n\n    computeCorrespondent() {\n        if (this.channel_type === \"channel\") {\n            return undefined;\n        }\n        const correspondents = this.correspondents;\n        if (correspondents.length === 1) {\n            // 2 members chat.\n            return correspondents[0];\n        }\n        if (correspondents.length === 0 && this.channelMembers.length === 1) {\n            // Self-chat.\n            return this.channelMembers[0];\n        }\n        return undefined;\n    }\n\n    computeIsDisplayed() {\n        return this.store.ChatWindow.get({ thread: this })?.isOpen;\n    }\n\n    get avatarUrl() {\n        return this.module_icon ?? this.store.DEFAULT_AVATAR;\n    }\n\n    get allowDescription() {\n        return [\"channel\", \"group\"].includes(this.channel_type);\n    }\n\n    get isTransient() {\n        return !this.id || this.id < 0;\n    }\n\n    get lastEditableMessageOfSelf() {\n        const editableMessagesBySelf = this.nonEmptyMessages.filter(\n            (message) => message.isSelfAuthored && message.editable\n        );\n        if (editableMessagesBySelf.length > 0) {\n            return editableMessagesBySelf.at(-1);\n        }\n        return null;\n    }\n\n    get needactionCounter() {\n        return this.isChatChannel\n            ? this.selfMember?.message_unread_counter ?? 0\n            : this.message_needaction_counter;\n    }\n\n    newestMessage = Record.one(\"Message\", {\n        inverse: \"threadAsNewest\",\n        compute() {\n            return this.messages.findLast((msg) => !msg.isEmpty);\n        },\n    });\n\n    firstUnreadMessage = Record.one(\"Message\", {\n        /** @this {import(\"models\").Thread} */\n        compute() {\n            if (!this.selfMember) {\n                return null;\n            }\n            const messages = this.nonEmptyMessages;\n            const separator = this.selfMember.localNewMessageSeparator;\n            if (separator === 0 && !this.loadOlder) {\n                return messages[0];\n            }\n            if (!separator || messages.length === 0 || messages.at(-1).id < separator) {\n                return null;\n            }\n            // try to find a perfect match according to the member's separator\n            let message = this.store.Message.get({ id: separator });\n            if (!message || this.notEq(message.thread) || message.isEmpty) {\n                message = nearestGreaterThanOrEqual(messages, separator, (msg) => msg.id);\n            }\n            return message;\n        },\n    });\n\n    get newestPersistentMessage() {\n        return this.messages.findLast((msg) => Number.isInteger(msg.id));\n    }\n\n    newestPersistentAllMessages = Record.many(\"Message\", {\n        compute() {\n            const allPersistentMessages = this.allMessages.filter((message) =>\n                Number.isInteger(message.id)\n            );\n            allPersistentMessages.sort((m1, m2) => m2.id - m1.id);\n            return allPersistentMessages;\n        },\n    });\n\n    newestPersistentOfAllMessage = Record.one(\"Message\", {\n        compute() {\n            return this.newestPersistentAllMessages[0];\n        },\n    });\n\n    newestPersistentNotEmptyOfAllMessage = Record.one(\"Message\", {\n        compute() {\n            return this.newestPersistentAllMessages.find((message) => !message.isEmpty);\n        },\n    });\n\n    get oldestPersistentMessage() {\n        return this.messages.find((msg) => Number.isInteger(msg.id));\n    }\n\n    onPinStateUpdated() {}\n\n    get hasSelfAsMember() {\n        return Boolean(this.selfMember);\n    }\n\n    hasSeenFeature = Record.attr(false, {\n        /** @this {import(\"models\").Thread} */\n        compute() {\n            return this.store.channel_types_with_seen_infos.includes(this.channel_type);\n        },\n    });\n\n    get invitationLink() {\n        if (!this.uuid || this.channel_type === \"chat\") {\n            return undefined;\n        }\n        return `${window.location.origin}/chat/${this.id}/${this.uuid}`;\n    }\n\n    get isEmpty() {\n        return !this.messages.some((message) => !message.isEmpty);\n    }\n\n    get nonEmptyMessages() {\n        return this.messages.filter((message) => !message.isEmpty);\n    }\n\n    get persistentMessages() {\n        return this.messages.filter((message) => !message.is_transient);\n    }\n\n    get prefix() {\n        return this.isChatChannel ? \"@\" : \"#\";\n    }\n\n    get showUnreadBanner() {\n        return !this.selfMember?.hideUnreadBanner && this.selfMember?.localMessageUnreadCounter > 0;\n    }\n\n    get rpcParams() {\n        return {};\n    }\n\n    /** @type {undefined|number[]} */\n    lastMessageSeenByAllId = Record.attr(undefined, {\n        compute() {\n            if (!this.hasSeenFeature) {\n                return;\n            }\n            const otherMembers = this.channelMembers.filter((member) =>\n                member.persona.notEq(this.store.self)\n            );\n            if (otherMembers.length === 0) {\n                return;\n            }\n            const otherLastSeenMessageIds = otherMembers\n                .filter((member) => member.seen_message_id)\n                .map((member) => member.seen_message_id.id);\n            if (otherLastSeenMessageIds.length === 0) {\n                return;\n            }\n            return Math.min(...otherLastSeenMessageIds);\n        },\n    });\n\n    lastSelfMessageSeenByEveryone = Record.one(\"Message\", {\n        compute() {\n            if (!this.lastMessageSeenByAllId) {\n                return false;\n            }\n            let res;\n            // starts from most recent persistent messages to find early\n            for (let i = this.persistentMessages.length - 1; i >= 0; i--) {\n                const message = this.persistentMessages[i];\n                if (!message.isSelfAuthored) {\n                    continue;\n                }\n                if (message.id > this.lastMessageSeenByAllId) {\n                    continue;\n                }\n                res = message;\n                break;\n            }\n            return res;\n        },\n    });\n\n    get unknownMembersCount() {\n        return this.memberCount - this.channelMembers.length;\n    }\n\n    executeCommand(command, body = \"\") {\n        return this.store.env.services.orm.call(\n            \"discuss.channel\",\n            command.methodName,\n            [[this.id]],\n            { body }\n        );\n    }\n\n    async fetchChannelMembers() {\n        if (this.fetchMembersState === \"pending\") {\n            return;\n        }\n        const previousState = this.fetchMembersState;\n        this.fetchMembersState = \"pending\";\n        const known_member_ids = this.channelMembers.map((channelMember) => channelMember.id);\n        let data;\n        try {\n            data = await rpc(\"/discuss/channel/members\", {\n                channel_id: this.id,\n                known_member_ids: known_member_ids,\n            });\n        } catch (e) {\n            this.fetchMembersState = previousState;\n            throw e;\n        }\n        this.fetchMembersState = \"fetched\";\n        this.store.insert(data);\n    }\n\n    /** @param {{after: Number, before: Number}} */\n    async fetchMessages({ after, around, before } = {}) {\n        this.status = \"loading\";\n        if (![\"mail.box\", \"discuss.channel\"].includes(this.model) && !this.id) {\n            this.isLoaded = true;\n            return [];\n        }\n        try {\n            const { data, messages } = await this.fetchMessagesData({ after, around, before });\n            this.store.insert(data, { html: true });\n            this.isLoaded = true;\n            return this.store.Message.insert(messages.reverse());\n        } catch (e) {\n            this.hasLoadingFailed = true;\n            throw e;\n        } finally {\n            this.status = \"ready\";\n        }\n    }\n\n    /** @param {{after: Number, before: Number}} */\n    async fetchMessagesData({ after, around, before } = {}) {\n        // ordered messages received: newest to oldest\n        return await rpc(this.getFetchRoute(), {\n            ...this.getFetchParams(),\n            limit: !around && around !== 0 ? this.store.FETCH_LIMIT : this.store.FETCH_LIMIT * 2,\n            after,\n            around,\n            before,\n        });\n    }\n\n    /** @param {\"older\"|\"newer\"} epoch */\n    async fetchMoreMessages(epoch = \"older\") {\n        if (\n            this.status === \"loading\" ||\n            (epoch === \"older\" && !this.loadOlder) ||\n            (epoch === \"newer\" && !this.loadNewer)\n        ) {\n            return;\n        }\n        const before = epoch === \"older\" ? this.oldestPersistentMessage?.id : undefined;\n        const after = epoch === \"newer\" ? this.newestPersistentMessage?.id : undefined;\n        try {\n            const fetched = await this.fetchMessages({ after, before });\n            if (\n                (after !== undefined && !this.messages.some((message) => message.id === after)) ||\n                (before !== undefined && !this.messages.some((message) => message.id === before))\n            ) {\n                // there might have been a jump to message during RPC fetch.\n                // Abort feeding messages as to not put holes in message list.\n                return;\n            }\n            const alreadyKnownMessages = new Set(this.messages.map(({ id }) => id));\n            const messagesToAdd = fetched.filter(\n                (message) => !alreadyKnownMessages.has(message.id)\n            );\n            if (epoch === \"older\") {\n                this.messages.unshift(...messagesToAdd);\n            } else {\n                this.messages.push(...messagesToAdd);\n            }\n            if (fetched.length < this.store.FETCH_LIMIT) {\n                if (epoch === \"older\") {\n                    this.loadOlder = false;\n                } else if (epoch === \"newer\") {\n                    this.loadNewer = false;\n                    const missingMessages = this.pendingNewMessages.filter(\n                        ({ id }) => !alreadyKnownMessages.has(id)\n                    );\n                    if (missingMessages.length > 0) {\n                        this.messages.push(...missingMessages);\n                        this.messages.sort((m1, m2) => m1.id - m2.id);\n                    }\n                }\n            }\n            this._enrichMessagesWithTransient();\n        } catch {\n            // handled in fetchMessages\n        }\n        this.pendingNewMessages = [];\n    }\n\n    async fetchNewMessages() {\n        if (\n            this.status === \"loading\" ||\n            (this.isLoaded && [\"discuss.channel\", \"mail.box\"].includes(this.model))\n        ) {\n            return;\n        }\n        const after = this.isLoaded ? this.newestPersistentMessage?.id : undefined;\n        try {\n            const fetched = await this.fetchMessages({ after });\n            // feed messages\n            // could have received a new message as notification during fetch\n            // filter out already fetched (e.g. received as notification in the meantime)\n            let startIndex;\n            if (after === undefined) {\n                startIndex = 0;\n            } else {\n                const afterIndex = this.messages.findIndex((message) => message.id === after);\n                if (afterIndex === -1) {\n                    // there might have been a jump to message during RPC fetch.\n                    // Abort feeding messages as to not put holes in message list.\n                    return;\n                } else {\n                    startIndex = afterIndex + 1;\n                }\n            }\n            const alreadyKnownMessages = new Set(this.messages.map((m) => m.id));\n            const filtered = fetched.filter(\n                (message) =>\n                    !alreadyKnownMessages.has(message.id) &&\n                    (this.persistentMessages.length === 0 ||\n                        message.id < this.oldestPersistentMessage.id ||\n                        message.id > this.newestPersistentMessage.id)\n            );\n            this.messages.splice(startIndex, 0, ...filtered);\n            Object.assign(this, {\n                loadOlder:\n                    after === undefined && fetched.length === this.store.FETCH_LIMIT\n                        ? true\n                        : after === undefined && fetched.length !== this.store.FETCH_LIMIT\n                        ? false\n                        : this.loadOlder,\n            });\n        } catch {\n            // handled in fetchMessages\n        }\n    }\n\n    getFetchParams() {\n        if (this.model === \"discuss.channel\") {\n            return { channel_id: this.id };\n        }\n        if (this.model === \"mail.box\") {\n            return {};\n        }\n        return {\n            thread_id: this.id,\n            thread_model: this.model,\n            ...this.rpcParams,\n        };\n    }\n\n    getFetchRoute() {\n        if (this.model === \"discuss.channel\") {\n            return \"/discuss/channel/messages\";\n        }\n        if (this.model === \"mail.box\" && this.id === \"inbox\") {\n            return `/mail/inbox/messages`;\n        }\n        if (this.model === \"mail.box\" && this.id === \"starred\") {\n            return `/mail/starred/messages`;\n        }\n        if (this.model === \"mail.box\" && this.id === \"history\") {\n            return `/mail/history/messages`;\n        }\n        return this.fetchRouteChatter;\n    }\n\n    get fetchRouteChatter() {\n        return \"/mail/thread/messages\";\n    }\n\n    async leave() {\n        await this.store.env.services.orm.call(\"discuss.channel\", \"action_unfollow\", [this.id]);\n    }\n\n    /**\n     * Get ready to jump to a message in a thread. This method will fetch the\n     * messages around the message to jump to if required, and update the thread\n     * messages accordingly.\n     *\n     * @param {import(\"models\").Message} [messageId] if not provided, load around newest message\n     */\n    async loadAround(messageId) {\n        if (\n            this.status === \"loading\" ||\n            (this.isLoaded && this.messages.some(({ id }) => id === messageId))\n        ) {\n            return;\n        }\n        try {\n            this.isLoaded = false;\n            this.scrollTop = undefined;\n            this.messages = await this.fetchMessages({ around: messageId });\n            this.isLoaded = true;\n            this.loadNewer = messageId !== undefined ? true : false;\n            this.loadOlder = true;\n            const limit =\n                !messageId && messageId !== 0 ? this.store.FETCH_LIMIT : this.store.FETCH_LIMIT * 2;\n            if (this.messages.length < limit) {\n                const olderMessagesCount = this.messages.filter(({ id }) => id < messageId).length;\n                const newerMessagesCount = this.messages.filter(({ id }) => id > messageId).length;\n                if (olderMessagesCount < limit / 2 - 1) {\n                    this.loadOlder = false;\n                }\n                if (newerMessagesCount < limit / 2) {\n                    this.loadNewer = false;\n                }\n            }\n            this._enrichMessagesWithTransient();\n        } catch {\n            // handled in fetchMessages\n        }\n    }\n\n    async markAllMessagesAsRead() {\n        await this.store.env.services.orm.silent.call(\"mail.message\", \"mark_all_as_read\", [\n            [\n                [\"model\", \"=\", this.model],\n                [\"res_id\", \"=\", this.id],\n            ],\n        ]);\n        this.message_needaction_counter = 0;\n    }\n\n    async markAsFetched() {\n        await this.store.env.services.orm.silent.call(\"discuss.channel\", \"channel_fetched\", [\n            [this.id],\n        ]);\n    }\n\n    /**\n     * @param {Object} [options]\n     * @param {boolean} [options.sync] Whether to sync the unread message\n     * state with the server values.\n     */\n    markAsRead({ sync } = {}) {\n        const newestPersistentMessage = this.newestPersistentOfAllMessage;\n        if (!newestPersistentMessage && !this.isLoaded) {\n            this.isLoadedDeferred.then(() => new Promise(setTimeout)).then(() => this.markAsRead());\n        }\n        const alreadyReadBySelf = newestPersistentMessage?.isReadBySelf;\n        if (this.selfMember) {\n            this.selfMember.syncUnread = sync ?? this.selfMember.syncUnread;\n            this.selfMember.seen_message_id = newestPersistentMessage;\n        }\n        if (newestPersistentMessage && this.selfMember && !alreadyReadBySelf) {\n            rpc(\"/discuss/channel/mark_as_read\", {\n                channel_id: this.id,\n                last_message_id: newestPersistentMessage.id,\n                sync,\n            }).catch((e) => {\n                if (e.code !== 404) {\n                    throw e;\n                }\n            });\n        }\n        if (this.message_needaction_counter > 0) {\n            this.markAllMessagesAsRead();\n        }\n    }\n\n    /** @param {string} data base64 representation of the binary */\n    async notifyAvatarToServer(data) {\n        await rpc(\"/discuss/channel/update_avatar\", {\n            channel_id: this.id,\n            data,\n        });\n    }\n\n    async notifyDescriptionToServer(description) {\n        this.description = description;\n        return this.store.env.services.orm.call(\n            \"discuss.channel\",\n            \"channel_change_description\",\n            [[this.id]],\n            { description }\n        );\n    }\n\n    /** @param {Object} [options] */\n    open(options) {}\n\n    openChatWindow({ fromMessagingMenu } = {}) {\n        const cw = this.store.ChatWindow.insert(\n            assignDefined({ thread: this }, { fromMessagingMenu })\n        );\n        this.store.chatHub.opened.delete(cw);\n        this.store.chatHub.opened.unshift(cw);\n        if (!isMobileOS()) {\n            cw.focus();\n        }\n        this.state = \"open\";\n        cw.notifyState();\n        return cw;\n    }\n\n    pin() {\n        if (this.model !== \"discuss.channel\" || this.store.self.type !== \"partner\") {\n            return;\n        }\n        this.is_pinned = true;\n        return this.store.env.services.orm.silent.call(\n            \"discuss.channel\",\n            \"channel_pin\",\n            [this.id],\n            { pinned: true }\n        );\n    }\n\n    /** @param {string} name */\n    async rename(name) {\n        const newName = name.trim();\n        if (\n            newName !== this.displayName &&\n            ((newName && this.channel_type === \"channel\") ||\n                this.channel_type === \"chat\" ||\n                this.channel_type === \"group\")\n        ) {\n            if (this.channel_type === \"channel\" || this.channel_type === \"group\") {\n                this.name = newName;\n                await this.store.env.services.orm.call(\n                    \"discuss.channel\",\n                    \"channel_rename\",\n                    [[this.id]],\n                    { name: newName }\n                );\n            } else if (this.channel_type === \"chat\") {\n                this.custom_channel_name = newName;\n                await this.store.env.services.orm.call(\n                    \"discuss.channel\",\n                    \"channel_set_custom_name\",\n                    [[this.id]],\n                    { name: newName }\n                );\n            }\n        }\n    }\n\n    addOrReplaceMessage(message, tmpMsg) {\n        // The message from other personas (not self) should not replace the tmpMsg\n        if (tmpMsg && tmpMsg.in(this.messages) && message.author.eq(this.store.self)) {\n            this.messages.splice(this.messages.indexOf(tmpMsg), 1, message);\n            return;\n        }\n        this.messages.add(message);\n    }\n\n    /** @param {string} body\n     *  @param {Object} extraData\n     */\n    async post(body, postData = {}, extraData = {}) {\n        let tmpMsg;\n        postData.attachments = postData.attachments ? [...postData.attachments] : []; // to not lose them on composer clear\n        const { attachments, parentId, mentionedChannels, mentionedPartners } = postData;\n        const params = await this.store.getMessagePostParams({ body, postData, thread: this });\n        Object.assign(params, extraData);\n        const tmpId = this.store.getNextTemporaryId();\n        params.context = { ...user.context, ...params.context, temporary_id: tmpId };\n        if (parentId) {\n            params.post_data.parent_id = parentId;\n        }\n        if (this.model !== \"discuss.channel\") {\n            params.thread_id = this.id;\n            params.thread_model = this.model;\n        } else {\n            const tmpData = {\n                id: tmpId,\n                attachments: attachments,\n                res_id: this.id,\n                model: \"discuss.channel\",\n            };\n            tmpData.author = this.store.self;\n            if (parentId) {\n                tmpData.parentMessage = this.store.Message.get(parentId);\n            }\n            const prettyContent = await prettifyMessageContent(\n                body,\n                this.store.getMentionsFromText(body, {\n                    mentionedChannels,\n                    mentionedPartners,\n                })\n            );\n            tmpMsg = this.store.Message.insert(\n                {\n                    ...tmpData,\n                    body: prettyContent,\n                    isPending: true,\n                    thread: this,\n                },\n                { html: true }\n            );\n            this.messages.push(tmpMsg);\n            if (this.selfMember) {\n                this.selfMember.syncUnread = true;\n                this.selfMember.seen_message_id = tmpMsg;\n                this.selfMember.new_message_separator = tmpMsg.id + 1;\n            }\n        }\n        const data = await this.store.doMessagePost(params, tmpMsg);\n        if (!data) {\n            return;\n        }\n        const { Message: messages = [] } = this.store.insert(data, { html: true });\n        const [message] = messages;\n        this.addOrReplaceMessage(message, tmpMsg);\n        if (this.selfMember?.seen_message_id?.id < message.id) {\n            this.selfMember.seen_message_id = message;\n            this.selfMember.new_message_separator = message.id + 1;\n        }\n        // Only delete the temporary message now that seen_message_id is updated\n        // to avoid flickering.\n        tmpMsg?.delete();\n        if (message.hasLink && this.store.hasLinkPreviewFeature) {\n            rpc(\"/mail/link_preview\", { message_id: message.id }, { silent: true });\n        }\n        return message;\n    }\n\n    /** @param {number} index */\n    async setMainAttachmentFromIndex(index) {\n        this.mainAttachment = this.attachmentsInWebClientView[index];\n        await this.store.env.services.orm.call(\"ir.attachment\", \"register_as_main_attachment\", [\n            this.mainAttachment.id,\n        ]);\n    }\n\n    /**\n     * Following a load more or load around, listing of messages contains persistent messages.\n     * Transient messages are missing, so this function puts known transient messages at the\n     * right place in message list of thread.\n     */\n    _enrichMessagesWithTransient() {\n        for (const message of this.transientMessages) {\n            if (message.id < this.oldestPersistentMessage && !this.loadOlder) {\n                this.messages.unshift(message);\n            } else if (message.id > this.newestPersistentMessage && !this.loadNewer) {\n                this.messages.push(message);\n            } else {\n                let afterIndex = this.messages.findIndex((msg) => msg.id > message.id);\n                if (afterIndex === -1) {\n                    afterIndex = this.messages.length + 1;\n                }\n                this.messages.splice(afterIndex - 1, 0, message);\n            }\n        }\n    }\n}\n\nThread.register();\n", "import { Record } from \"./record\";\n\nexport class Volume extends Record {\n    static id = \"persona\";\n\n    persona = Record.one(\"Persona\");\n    volume = 1;\n}\n\nVolume.register();\n", "import { Component, useState } from \"@odoo/owl\";\nimport { ResizablePanel } from \"@web/core/resizable_panel/resizable_panel\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @prop {string} title\n * @prop {Object} [slots]\n * @extends {Component<Props, Env>}\n */\nexport class ActionPanel extends Component {\n    static template = \"mail.ActionPanel\";\n    static components = { ResizablePanel };\n    static props = [\"icon?\", \"title?\", \"resizable?\", \"slots?\", \"initialWidth?\", \"minWidth?\"];\n    static defaultProps = { resizable: true };\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n    }\n\n    get classNames() {\n        return `o-mail-ActionPanel overflow-auto d-flex flex-column flex-shrink-0 position-relative py-2 pt-0 h-100 bg-inherit ${\n            !this.env.inChatter ? \" px-2\" : \" o-mail-ActionPanel-chatter\"\n        } ${this.env.inDiscussApp ? \" o-mail-discussSidebarBgColor\" : \"\"}`;\n    }\n}\n", "import { Attachment } from \"@mail/core/common/attachment_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Attachment.prototype, {\n    get isDeletable() {\n        if (this.message && this.thread?.model === \"discuss.channel\") {\n            return this.message.editable;\n        }\n        return super.isDeletable;\n    },\n    get urlRoute() {\n        if (!this.access_token && this.thread?.model === \"discuss.channel\") {\n            return this.isImage\n                ? `/discuss/channel/${this.thread.id}/image/${this.id}`\n                : `/discuss/channel/${this.thread.id}/attachment/${this.id}`;\n        }\n        return super.urlRoute;\n    },\n});\n", "import { DateSection } from \"@mail/core/common/date_section\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\nimport { AttachmentList } from \"@mail/core/common/attachment_list\";\n\nimport { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useSequential, useVisible } from \"@mail/utils/common/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Thread} thread\n */\nexport class AttachmentPanel extends Component {\n    static components = { ActionPanel, AttachmentList, DateSection };\n    static props = [\"thread\"];\n    static template = \"mail.AttachmentPanel\";\n\n    setup() {\n        super.setup();\n        this.sequential = useSequential();\n        this.store = useService(\"mail.store\");\n        this.ormService = useService(\"orm\");\n        this.attachmentUploadService = useService(\"mail.attachment_upload\");\n        onWillStart(() => {\n            this.props.thread.fetchMoreAttachments();\n        });\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.thread.notEq(this.props.thread)) {\n                nextProps.thread.fetchMoreAttachments();\n            }\n        });\n        useVisible(\"load-older\", (isVisible) => {\n            if (isVisible) {\n                this.props.thread.fetchMoreAttachments();\n            }\n        });\n    }\n\n    /**\n     * @return {Object<string, import(\"models\").Attachment[]>}\n     */\n    get attachmentsByDate() {\n        const attachmentsByDate = {};\n        for (const attachment of this.props.thread.attachments) {\n            const attachments = attachmentsByDate[attachment.monthYear] ?? [];\n            attachments.push(attachment);\n            attachmentsByDate[attachment.monthYear] = attachments;\n        }\n        return attachmentsByDate;\n    }\n\n    get hasToggleAllowPublicUpload() {\n        return (\n            this.props.thread.model !== \"mail.box\" &&\n            this.props.thread.channel_type !== \"chat\" &&\n            this.store.self.isInternalUser\n        );\n    }\n\n    toggleAllowPublicUpload() {\n        this.sequential(() =>\n            this.ormService.write(\"discuss.channel\", [this.props.thread.id], {\n                allow_public_upload: !this.props.thread.allow_public_upload,\n            })\n        );\n    }\n}\n", "import { AttachmentUploadService } from \"@mail/core/common/attachment_upload_service\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(AttachmentUploadService.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.env.services[\"bus_service\"].subscribe(\"mail.record/insert\", ({ Thread }) => {\n            if (\n                Thread &&\n                \"allow_public_upload\" in Thread &&\n                !Thread.allow_public_upload &&\n                !this.store.self.isInternalUser\n            ) {\n                const attachments = [...this.store.Thread.insert(Thread).composer.attachments];\n                for (const attachment of attachments) {\n                    this.unlink(attachment);\n                }\n            }\n        });\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nconst commandRegistry = registry.category(\"discuss.channel_commands\");\n\ncommandRegistry\n    .add(\"help\", {\n        help: _t(\"Show a helper message\"),\n        methodName: \"execute_command_help\",\n    })\n    .add(\"leave\", {\n        help: _t(\"Leave this channel\"),\n        methodName: \"execute_command_leave\",\n    })\n    .add(\"who\", {\n        channel_types: [\"channel\", \"chat\", \"group\"],\n        help: _t(\"List users in the current channel\"),\n        methodName: \"execute_command_who\",\n    });\n", "import { ImStatus } from \"@mail/core/common/im_status\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\n\nimport { Component, onMounted, onWillStart, useRef, useState } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useSequential } from \"@mail/utils/common/hooks\";\n\nexport class ChannelInvitation extends Component {\n    static components = { ImStatus, ActionPanel };\n    static defaultProps = { hasSizeConstraints: false };\n    static props = [\"hasSizeConstraints?\", \"thread\", \"close\", \"className?\"];\n    static template = \"discuss.ChannelInvitation\";\n\n    setup() {\n        super.setup();\n        this.discussCoreCommonService = useState(useService(\"discuss.core.common\"));\n        this.orm = useService(\"orm\");\n        this.store = useState(useService(\"mail.store\"));\n        this.notification = useService(\"notification\");\n        this.suggestionService = useService(\"mail.suggestion\");\n        this.ui = useService(\"ui\");\n        this.inputRef = useRef(\"input\");\n        this.sequential = useSequential();\n        this.searchStr = \"\";\n        this.state = useState({\n            selectablePartners: [],\n            selectedPartners: [],\n            searchResultCount: 0,\n        });\n        onWillStart(() => {\n            if (this.store.self.type === \"partner\") {\n                this.fetchPartnersToInvite();\n            }\n        });\n        onMounted(() => {\n            if (this.store.self.type === \"partner\") {\n                this.inputRef.el.focus();\n            }\n        });\n    }\n\n    async fetchPartnersToInvite() {\n        const results = await this.sequential(() =>\n            this.orm.call(\"res.partner\", \"search_for_channel_invite\", [\n                this.searchStr,\n                this.props.thread.id,\n            ])\n        );\n        if (!results) {\n            return;\n        }\n        const { Persona: selectablePartners = [] } = this.store.insert(results.data);\n        this.state.selectablePartners = this.suggestionService.sortPartnerSuggestions(\n            selectablePartners,\n            this.searchStr,\n            this.props.thread\n        );\n        this.state.searchResultCount = results[\"count\"];\n    }\n\n    onInput() {\n        this.searchStr = this.inputRef.el.value;\n        this.fetchPartnersToInvite();\n    }\n\n    onClickSelectablePartner(partner) {\n        if (partner.in(this.state.selectedPartners)) {\n            const index = this.state.selectedPartners.indexOf(partner);\n            if (index !== -1) {\n                this.state.selectedPartners.splice(index, 1);\n            }\n            return;\n        }\n        this.state.selectedPartners.push(partner);\n    }\n\n    onClickSelectedPartner(partner) {\n        const index = this.state.selectedPartners.indexOf(partner);\n        this.state.selectedPartners.splice(index, 1);\n    }\n\n    onFocusInvitationLinkInput(ev) {\n        ev.target.select();\n    }\n\n    async onClickCopy(ev) {\n        await navigator.clipboard.writeText(this.props.thread.invitationLink);\n        this.notification.add(_t(\"Link copied!\"), { type: \"success\" });\n    }\n\n    async onClickInvite() {\n        if (this.props.thread.channel_type === \"chat\") {\n            const partnerIds = this.state.selectedPartners.map((partner) => partner.id);\n            if (this.props.thread.correspondent) {\n                partnerIds.unshift(this.props.thread.correspondent.persona.id);\n            }\n            await this.discussCoreCommonService.startChat(partnerIds);\n        } else {\n            await this.orm.call(\"discuss.channel\", \"add_members\", [[this.props.thread.id]], {\n                partner_ids: this.state.selectedPartners.map((partner) => partner.id),\n            });\n        }\n        this.props.close();\n    }\n\n    get invitationButtonText() {\n        if (this.props.thread.channel_type === \"channel\") {\n            return _t(\"Invite to Channel\");\n        } else if (this.props.thread.channel_type === \"group\") {\n            return _t(\"Invite to Group Chat\");\n        } else if (this.props.thread.channel_type === \"chat\") {\n            if (this.props.thread.correspondent?.persona.eq(this.store.self)) {\n                if (this.state.selectedPartners.length === 0) {\n                    return _t(\"Invite\");\n                }\n                if (this.state.selectedPartners.length === 1) {\n                    const alreadyChat = Object.values(this.store.Thread.records).some((thread) =>\n                        thread.correspondent?.persona.eq(this.state.selectedPartners[0])\n                    );\n                    if (alreadyChat) {\n                        return _t(\"Go to conversation\");\n                    }\n                    return _t(\"Start a Conversation\");\n                }\n            }\n            return _t(\"Create Group Chat\");\n        }\n        return _t(\"Invite\");\n    }\n}\n", "import { ImStatus } from \"@mail/core/common/im_status\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\n\nimport { Component, onWillUpdateProps, onWillStart, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class ChannelMemberList extends Component {\n    static components = { ImStatus, ActionPanel };\n    static props = [\"thread\", \"openChannelInvitePanel\", \"className?\"];\n    static template = \"discuss.ChannelMemberList\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        onWillStart(() => {\n            if (this.props.thread.fetchMembersState === \"not_fetched\") {\n                this.props.thread.fetchChannelMembers();\n            }\n        });\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.thread.fetchMembersState === \"not_fetched\") {\n                nextProps.thread.fetchChannelMembers();\n            }\n        });\n    }\n\n    get onlineSectionText() {\n        return _t(\"Online - %(online_count)s\", {\n            online_count: this.props.thread.onlineMembers.length,\n        });\n    }\n\n    get offlineSectionText() {\n        return _t(\"Offline - %(offline_count)s\", {\n            offline_count: this.props.thread.offlineMembers.length,\n        });\n    }\n\n    canOpenChatWith(member) {\n        if (this.store.inPublicPage) {\n            return false;\n        }\n        if (member.persona.type === \"guest\") {\n            return false;\n        }\n        return true;\n    }\n\n    onClickAvatar(ev, member) {\n        if (!this.canOpenChatWith(member)) {\n            return;\n        }\n        this.store.openChat({ partnerId: member.persona.id });\n    }\n}\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Composer.prototype, {\n    get allowUpload() {\n        const thread = this.thread ?? this.message.thread;\n        return (\n            super.allowUpload &&\n            (thread.model !== \"discuss.channel\" ||\n                thread?.allow_public_upload ||\n                this.store.self.isInternalUser)\n        );\n    },\n});\n", "import { reactive } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nexport class DiscussCoreCommon {\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    constructor(env, services) {\n        this.busService = services.bus_service;\n        this.env = env;\n        this.notificationService = services.notification;\n        this.orm = services.orm;\n        this.presence = services.presence;\n        this.store = services[\"mail.store\"];\n    }\n\n    setup() {\n        this.busService.addEventListener(\n            \"connect\",\n            () =>\n                this.store.imStatusTrackedPersonas.forEach((p) => {\n                    const model = p.type === \"partner\" ? \"res.partner\" : \"mail.guest\";\n                    this.busService.addChannel(`odoo-presence-${model}_${p.id}`);\n                }),\n            { once: true }\n        );\n        this.busService.subscribe(\"discuss.channel/leave\", (payload) => {\n            const { Thread } = this.store.insert(payload);\n            const [thread] = Thread;\n            if (thread.notifyOnLeave) {\n                this.notificationService.add(_t(\"You unsubscribed from %s.\", thread.displayName), {\n                    type: \"info\",\n                });\n            }\n        });\n        this.busService.subscribe(\"discuss.channel/delete\", (payload, metadata) => {\n            const thread = this.store.Thread.insert({\n                id: payload.id,\n                model: \"discuss.channel\",\n            });\n            this._handleNotificationChannelDelete(thread, metadata);\n        });\n        this.busService.subscribe(\"discuss.channel/new_message\", (payload, metadata) =>\n            this._handleNotificationNewMessage(payload, metadata)\n        );\n        this.busService.subscribe(\"discuss.channel/transient_message\", (payload) => {\n            const { body, thread } = payload;\n            const lastMessageId = this.store.getLastMessageId();\n            const message = this.store.Message.insert(\n                {\n                    author: this.store.odoobot,\n                    body,\n                    id: lastMessageId + 0.01,\n                    is_note: true,\n                    is_transient: true,\n                    thread,\n                },\n                { html: true }\n            );\n            message.thread.messages.push(message);\n            message.thread.transientMessages.push(message);\n        });\n        this.busService.subscribe(\"discuss.channel/unpin\", (payload) => {\n            const thread = this.store.Thread.get({ model: \"discuss.channel\", id: payload.id });\n            if (thread) {\n                thread.is_pinned = false;\n                this.notificationService.add(\n                    thread.parent_channel_id\n                        ? _t(`You unpinned %(conversation_name)s`, {\n                              conversation_name: thread.displayName,\n                          })\n                        : _t(`You unpinned your conversation with %(user_name)s`, {\n                              user_name: thread.displayName,\n                          }),\n                    { type: \"info\" }\n                );\n            }\n        });\n        this.busService.subscribe(\"discuss.channel.member/fetched\", (payload) => {\n            const { channel_id, id, last_message_id, partner_id } = payload;\n            this.store.ChannelMember.insert({\n                id,\n                fetched_message_id: { id: last_message_id },\n                persona: { type: \"partner\", id: partner_id },\n                thread: { id: channel_id, model: \"discuss.channel\" },\n            });\n        });\n        this.env.bus.addEventListener(\"mail.message/delete\", ({ detail: { message, notifId } }) => {\n            if (message.thread) {\n                const { selfMember } = message.thread;\n                if (\n                    message.id > selfMember?.seen_message_id.id &&\n                    notifId > selfMember.message_unread_counter_bus_id\n                ) {\n                    selfMember.message_unread_counter--;\n                }\n            }\n        });\n    }\n\n    async createGroupChat({ default_display_mode, partners_to }) {\n        const data = await this.orm.call(\"discuss.channel\", \"create_group\", [], {\n            default_display_mode,\n            partners_to,\n        });\n        const { Thread } = this.store.insert(data);\n        const [channel] = Thread;\n        channel.open();\n        return channel;\n    }\n\n    /** @param {[number]} partnerIds */\n    async startChat(partnerIds) {\n        const partners_to = [...new Set([this.store.self.id, ...partnerIds])];\n        if (partners_to.length === 1) {\n            const chat = await this.store.joinChat(partners_to[0], true);\n            this.store.ChatWindow?.get({ thread: undefined })?.close();\n            chat.open();\n        } else if (partners_to.length === 2) {\n            const correspondentId = partners_to.find(\n                (partnerId) => partnerId !== this.store.self.id\n            );\n            const chat = await this.store.joinChat(correspondentId, true);\n            chat.open();\n        } else {\n            await this.createGroupChat({ partners_to });\n        }\n    }\n\n    /**\n     * @param {import(\"models\").Thread} thread\n     * @param {{ notifId: number}} metadata\n     */\n    _handleNotificationChannelDelete(thread, metadata) {\n        thread.closeChatWindow();\n        thread.messages.splice(0, thread.messages.length);\n        thread.delete();\n    }\n\n    async _handleNotificationNewMessage(payload, { id: notifId }) {\n        const { data, id: channelId, temporary_id } = payload;\n        const channel = await this.store.Thread.getOrFetch({\n            model: \"discuss.channel\",\n            id: channelId,\n        });\n        if (!channel) {\n            return;\n        }\n        const { Message: messages = [] } = this.store.insert(data, { html: true });\n        const message = messages[0];\n        if (message.notIn(channel.messages)) {\n            if (!channel.loadNewer) {\n                channel.addOrReplaceMessage(message, this.store.Message.get(temporary_id));\n            } else if (channel.status === \"loading\") {\n                channel.pendingNewMessages.push(message);\n            }\n            if (message.isSelfAuthored && channel.selfMember) {\n                channel.selfMember.seen_message_id = message;\n            } else {\n                if (!channel.isDisplayed && channel.selfMember) {\n                    channel.selfMember.syncUnread = true;\n                    channel.scrollUnread = true;\n                }\n                if (notifId > channel.selfMember?.message_unread_counter_bus_id) {\n                    channel.selfMember.message_unread_counter++;\n                }\n            }\n        }\n        if (\n            !channel.isCorrespondentOdooBot &&\n            channel.channel_type !== \"channel\" &&\n            this.store.self.type === \"partner\"\n        ) {\n            // disabled on non-channel threads and\n            // on \"channel\" channels for performance reasons\n            channel.markAsFetched();\n        }\n        if (\n            !channel.loadNewer &&\n            !message.isSelfAuthored &&\n            channel.composer.isFocused &&\n            this.store.self.type === \"partner\" &&\n            channel.newestPersistentMessage?.eq(channel.newestMessage)\n        ) {\n            channel.markAsRead();\n        }\n        this.env.bus.trigger(\"discuss.channel/new_message\", { channel, message });\n        const authorMember = channel.channelMembers.find(({ persona }) =>\n            persona?.eq(message.author)\n        );\n        if (authorMember) {\n            authorMember.seen_message_id = message;\n        }\n    }\n}\n\nexport const discussCoreCommon = {\n    dependencies: [\n        \"bus_service\",\n        \"mail.out_of_focus\",\n        \"mail.store\",\n        \"notification\",\n        \"orm\",\n        \"presence\",\n    ],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    start(env, services) {\n        const discussCoreCommon = reactive(new DiscussCoreCommon(env, services));\n        discussCoreCommon.setup(env, services);\n        return discussCoreCommon;\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.core.common\", discussCoreCommon);\n", "import { Component, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class DiscussNotificationSettings extends Component {\n    static props = {};\n    static template = \"mail.DiscussNotificationSettings\";\n\n    setup() {\n        this.store = useState(useService(\"mail.store\"));\n        this.state = useState({\n            selectedDuration: false,\n        });\n    }\n\n    onChangeDisplayMuteDetails() {\n        // set the default mute duration to forever when opens the mute details\n        if (!this.store.settings.mute_until_dt) {\n            const FOREVER = this.store.settings.MUTES.find((m) => m.label === \"forever\").value;\n            this.store.settings.setMuteDuration(FOREVER);\n            this.state.selectedDuration = FOREVER;\n        } else {\n            this.store.settings.setMuteDuration(false);\n        }\n    }\n\n    onChangeMuteDuration(ev) {\n        if (ev.target.value === \"default\") {\n            return;\n        }\n        this.store.settings.setMuteDuration(parseInt(ev.target.value));\n        this.state.selectedDuration = parseInt(ev.target.value);\n    }\n}\n", "import { Component, xml } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nimport { DiscussNotificationSettings } from \"@mail/discuss/core/common/discuss_notification_settings\";\n\nexport class DiscussNotificationSettingsClientAction extends Component {\n    static components = { DiscussNotificationSettings };\n    static props = [\"*\"];\n    static template = xml`\n        <div class=\"o-mail-DiscussNotificationSettingsClientAction mx-3 my-2\">\n            <DiscussNotificationSettings/>\n        </div>\n    `;\n}\n\nregistry\n    .category(\"actions\")\n    .add(\"mail.discuss_notification_settings_action\", DiscussNotificationSettingsClientAction);\n", "import { messageActionsRegistry } from \"@mail/core/common/message_actions\";\nimport { patch } from \"@web/core/utils/patch\";\n\nconst editAction = messageActionsRegistry.get(\"edit\");\n\npatch(editAction, {\n    onClick(component) {\n        const body = new DOMParser().parseFromString(component.message.body, \"text/html\");\n        const mentionedChannelElements = body.querySelectorAll(\".o_channel_redirect\");\n        component.message.mentionedChannelPromises = Array.from(mentionedChannelElements)\n            .filter((el) => el.dataset.oeModel === \"discuss.channel\")\n            .map(async (el) => {\n                return component.store.Thread.getOrFetch({\n                    id: el.dataset.oeId,\n                    model: el.dataset.oeModel,\n                });\n            });\n        return super.onClick(component);\n    },\n});\n", "import { Message } from \"@mail/core/common/message_model\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    /** @type {Promise[]} */\n    mentionedChannelPromises: [],\n    /**\n     * @override\n     */\n    async edit(body, attachments = [], { mentionedChannels = [], mentionedPartners = [] } = {}) {\n        const validChannels = (await Promise.all(this.mentionedChannelPromises)).filter(\n            (channel) => channel !== undefined\n        );\n        const allChannels = this.store.Thread.insert([...validChannels, ...mentionedChannels]);\n        super.edit(body, attachments, {\n            mentionedChannels: allChannels,\n            mentionedPartners,\n        });\n    },\n});\n", "import { Component, useState, xml } from \"@odoo/owl\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { DiscussNotificationSettingsClientAction } from \"./discuss_notification_settings_client_action\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nclass NotificationDialog extends Component {\n    static props = [\"close?\"];\n    static components = { Dialog, DiscussNotificationSettingsClientAction };\n    static template = xml`\n        <Dialog size=\"'md'\" footer=\"false\">\n            <DiscussNotificationSettingsClientAction/>\n        </Dialog>\n    `;\n}\n\nexport class NotificationSettings extends Component {\n    static components = { ActionPanel, Dropdown, DropdownItem };\n    static props = [\"hasSizeConstraints?\", \"thread\", \"close?\", \"className?\"];\n    static template = \"discuss.NotificationSettings\";\n\n    setup() {\n        this.store = useState(useService(\"mail.store\"));\n        this.dialog = useService(\"dialog\");\n    }\n\n    setMute(minutes) {\n        this.store.settings.setMuteDuration(minutes, this.props.thread);\n        this.props.close?.();\n    }\n\n    onClickAllConversationsMuted() {\n        this.dialog.add(NotificationDialog);\n    }\n}\n", "import { partnerCompareRegistry } from \"@mail/core/common/partner_compare\";\n\npartnerCompareRegistry.add(\n    \"discuss.recent-chats\",\n    (p1, p2, { env, context }) => {\n        const recentChatPartnerIds =\n            context.recentChatPartnerIds || env.services[\"mail.store\"].getRecentChatPartnerIds();\n        const recentChatIndex_p1 = recentChatPartnerIds.findIndex(\n            (partnerId) => partnerId === p1.id\n        );\n        const recentChatIndex_p2 = recentChatPartnerIds.findIndex(\n            (partnerId) => partnerId === p2.id\n        );\n        if (recentChatIndex_p1 !== -1 && recentChatIndex_p2 === -1) {\n            return -1;\n        } else if (recentChatIndex_p1 === -1 && recentChatIndex_p2 !== -1) {\n            return 1;\n        } else if (recentChatIndex_p1 < recentChatIndex_p2) {\n            return -1;\n        } else if (recentChatIndex_p1 > recentChatIndex_p2) {\n            return 1;\n        }\n    },\n    { sequence: 25 }\n);\n\npartnerCompareRegistry.add(\n    \"discuss.members\",\n    (p1, p2, { thread, memberPartnerIds }) => {\n        if (thread?.model === \"discuss.channel\") {\n            const isMember1 = memberPartnerIds.has(p1.id);\n            const isMember2 = memberPartnerIds.has(p2.id);\n            if (isMember1 && !isMember2) {\n                return -1;\n            }\n            if (!isMember1 && isMember2) {\n                return 1;\n            }\n        }\n    },\n    { sequence: 40 }\n);\n", "import { Store } from \"@mail/core/common/store_service\";\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Store} */\nconst storeServicePatch = {\n    get onlineMemberStatuses() {\n        return [\"away\", \"bot\", \"online\"];\n    },\n    onLinkFollowed(fromThread) {\n        super.onLinkFollowed(...arguments);\n        if (!this.env.isSmall && fromThread?.model === \"discuss.channel\") {\n            fromThread.open(true, { autofocus: false });\n        }\n    },\n    sortMembers(m1, m2) {\n        return m1.persona.name?.localeCompare(m2.persona.name) || m1.id - m2.id;\n    },\n};\n\npatch(Store.prototype, storeServicePatch);\n", "import { SuggestionService } from \"@mail/core/common/suggestion_service\";\nimport { cleanTerm } from \"@mail/utils/common/format\";\n\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\n\nconst commandRegistry = registry.category(\"discuss.channel_commands\");\n\npatch(SuggestionService.prototype, {\n    getSupportedDelimiters(thread) {\n        const res = super.getSupportedDelimiters(thread);\n        return thread?.model === \"discuss.channel\" ? [...res, [\"/\", 0]] : res;\n    },\n    /**\n     * @override\n     */\n    searchSuggestions({ delimiter, term }, { thread, sort = false } = {}) {\n        if (delimiter === \"/\") {\n            return this.searchChannelCommand(cleanTerm(term), thread, sort);\n        }\n        return super.searchSuggestions(...arguments);\n    },\n    searchChannelCommand(cleanedSearchTerm, thread, sort) {\n        if (!thread.model === \"discuss.channel\") {\n            // channel commands are channel specific\n            return;\n        }\n        const commands = commandRegistry\n            .getEntries()\n            .filter(([name, command]) => {\n                if (!cleanTerm(name).includes(cleanedSearchTerm)) {\n                    return false;\n                }\n                if (command.channel_types) {\n                    return command.channel_types.includes(thread.channel_type);\n                }\n                return true;\n            })\n            .map(([name, command]) => {\n                return {\n                    channel_types: command.channel_types,\n                    help: command.help,\n                    id: command.id,\n                    name,\n                };\n            });\n        const sortFunc = (c1, c2) => {\n            if (c1.channel_types && !c2.channel_types) {\n                return -1;\n            }\n            if (!c1.channel_types && c2.channel_types) {\n                return 1;\n            }\n            const cleanedName1 = cleanTerm(c1.name);\n            const cleanedName2 = cleanTerm(c2.name);\n            if (\n                cleanedName1.startsWith(cleanedSearchTerm) &&\n                !cleanedName2.startsWith(cleanedSearchTerm)\n            ) {\n                return -1;\n            }\n            if (\n                !cleanedName1.startsWith(cleanedSearchTerm) &&\n                cleanedName2.startsWith(cleanedSearchTerm)\n            ) {\n                return 1;\n            }\n            if (cleanedName1 < cleanedName2) {\n                return -1;\n            }\n            if (cleanedName1 > cleanedName2) {\n                return 1;\n            }\n            return c1.id - c2.id;\n        };\n        return {\n            type: \"ChannelCommand\",\n            suggestions: sort ? commands.sort(sortFunc) : commands,\n        };\n    },\n});\n", "import { threadActionsRegistry } from \"@mail/core/common/thread_actions\";\nimport { AttachmentPanel } from \"@mail/discuss/core/common/attachment_panel\";\nimport { ChannelInvitation } from \"@mail/discuss/core/common/channel_invitation\";\nimport { ChannelMemberList } from \"@mail/discuss/core/common/channel_member_list\";\nimport { NotificationSettings } from \"@mail/discuss/core/common/notification_settings\";\n\nimport { useComponent } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\n\nthreadActionsRegistry\n    .add(\"notification-settings\", {\n        condition(component) {\n            return (\n                component.thread?.model === \"discuss.channel\" &&\n                component.store.self.type !== \"guest\" &&\n                (!component.props.chatWindow || component.props.chatWindow.isOpen)\n            );\n        },\n        setup(action) {\n            const component = useComponent();\n            if (!component.props.chatWindow) {\n                action.popover = usePopover(NotificationSettings, {\n                    onClose: () => action.close(),\n                    position: \"bottom-end\",\n                    fixedPosition: true,\n                    popoverClass: action.panelOuterClass,\n                });\n            }\n        },\n        open(component, action) {\n            action.popover?.open(component.root.el.querySelector(`[name=\"${action.id}\"]`), {\n                hasSizeConstraints: true,\n                thread: component.thread,\n            });\n        },\n        close(component, action) {\n            action.popover?.close();\n        },\n        component: NotificationSettings,\n        icon(component) {\n            return component.thread.isMuted\n                ? \"fa fa-fw text-danger fa-bell-slash\"\n                : \"fa fa-fw fa-bell\";\n        },\n        iconLarge(component) {\n            return component.thread.isMuted\n                ? \"fa fa-fw fa-lg text-danger fa-bell-slash\"\n                : \"fa fa-fw fa-lg fa-bell\";\n        },\n        name: _t(\"Notification Settings\"),\n        sequence: 10,\n        sequenceGroup: 30,\n        toggle: true,\n    })\n    .add(\"attachments\", {\n        condition: (component) =>\n            component.thread?.hasAttachmentPanel &&\n            (!component.props.chatWindow || component.props.chatWindow.isOpen),\n        component: AttachmentPanel,\n        icon: \"fa fa-fw fa-paperclip\",\n        iconLarge: \"fa fa-fw fa-lg fa-paperclip\",\n        name: _t(\"Attachments\"),\n        sequence: 10,\n        sequenceGroup: 10,\n        toggle: true,\n    })\n    .add(\"invite-people\", {\n        close(component, action) {\n            action.popover?.close();\n        },\n        component: ChannelInvitation,\n        componentProps(action) {\n            return { close: () => action.close() };\n        },\n        condition(component) {\n            return (\n                component.thread?.model === \"discuss.channel\" &&\n                (!component.props.chatWindow || component.props.chatWindow.isOpen)\n            );\n        },\n        panelOuterClass(component) {\n            return `o-discuss-ChannelInvitation ${component.props.chatWindow ? \"bg-inherit\" : \"\"}`;\n        },\n        icon: \"fa fa-fw fa-user-plus\",\n        iconLarge: \"fa fa-fw fa-lg fa-user-plus\",\n        name: _t(\"Invite People\"),\n        open(component, action) {\n            action.popover?.open(component.root.el.querySelector(`[name=\"${action.id}\"]`), {\n                hasSizeConstraints: true,\n                thread: component.thread,\n            });\n        },\n        sequence: 10,\n        sequenceGroup: 20,\n        setup(action) {\n            const component = useComponent();\n            if (!component.props.chatWindow) {\n                action.popover = usePopover(ChannelInvitation, {\n                    onClose: () => action.close(),\n                    popoverClass: action.panelOuterClass,\n                });\n            }\n        },\n        toggle: true,\n    })\n    .add(\"member-list\", {\n        component: ChannelMemberList,\n        condition(component) {\n            return (\n                component.thread?.hasMemberList &&\n                (!component.props.chatWindow || component.props.chatWindow.isOpen)\n            );\n        },\n        componentProps(action, component) {\n            return {\n                openChannelInvitePanel({ keepPrevious } = {}) {\n                    component.threadActions.actions\n                        .find(({ id }) => id === \"invite-people\")\n                        ?.open({ keepPrevious });\n                },\n            };\n        },\n        panelOuterClass: \"o-discuss-ChannelMemberList bg-inherit\",\n        icon: \"fa fa-fw fa-users\",\n        iconLarge: \"fa fa-fw fa-lg fa-users\",\n        name: _t(\"Members\"),\n        sequence: 30,\n        sequenceGroup: 10,\n        toggle: true,\n    });\n", "import { Record } from \"@mail/core/common/record\";\nimport { Thread } from \"@mail/core/common/thread_model\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { Mutex } from \"@web/core/utils/concurrency\";\nimport { registry } from \"@web/core/registry\";\n\nconst commandRegistry = registry.category(\"discuss.channel_commands\");\n\n/** @type {import(\"models\").Thread} */\nconst threadPatch = {\n    setup() {\n        super.setup();\n        this.fetchChannelMutex = new Mutex();\n        this.fetchChannelInfoDeferred = undefined;\n        this.fetchChannelInfoState = \"not_fetched\";\n        this.onlineMembers = Record.many(\"ChannelMember\", {\n            /** @this {import(\"models\").Thread} */\n            compute() {\n                return this.channelMembers.filter((member) =>\n                    this.store.onlineMemberStatuses.includes(member.persona.im_status)\n                );\n            },\n            sort(m1, m2) {\n                return this.store.sortMembers(m1, m2);\n            },\n        });\n        this.offlineMembers = Record.many(\"ChannelMember\", {\n            compute: this._computeOfflineMembers,\n            sort(m1, m2) {\n                return this.store.sortMembers(m1, m2);\n            },\n        });\n    },\n    _computeOfflineMembers() {\n        return this.channelMembers.filter(\n            (member) => !this.store.onlineMemberStatuses.includes(member.persona?.im_status)\n        );\n    },\n    get avatarUrl() {\n        if (this.channel_type === \"channel\" || this.channel_type === \"group\") {\n            return imageUrl(\"discuss.channel\", this.id, \"avatar_128\", {\n                unique: this.avatarCacheKey,\n            });\n        }\n        if (this.channel_type === \"chat\" && this.correspondent) {\n            return this.correspondent.persona.avatarUrl;\n        }\n        return super.avatarUrl;\n    },\n    get hasMemberList() {\n        return [\"channel\", \"group\"].includes(this.channel_type);\n    },\n    async fetchChannelInfo() {\n        return this.fetchChannelMutex.exec(async () => {\n            if (!(this.localId in this.store.Thread.records)) {\n                return; // channel was deleted in-between two calls\n            }\n            const data = await rpc(\"/discuss/channel/info\", { channel_id: this.id });\n            if (data) {\n                this.store.insert(data);\n            } else {\n                this.delete();\n            }\n            return data ? this : undefined;\n        });\n    },\n    async fetchMoreAttachments(limit = 30) {\n        if (this.isLoadingAttachments || this.areAttachmentsLoaded) {\n            return;\n        }\n        this.isLoadingAttachments = true;\n        try {\n            const data = await rpc(\"/discuss/channel/attachments\", {\n                before: Math.min(...this.attachments.map(({ id }) => id)),\n                channel_id: this.id,\n                limit,\n            });\n            const { Attachment: attachments = [] } = this.store.insert(data);\n            if (attachments.length < limit) {\n                this.areAttachmentsLoaded = true;\n            }\n        } finally {\n            this.isLoadingAttachments = false;\n        }\n    },\n    get notifyOnLeave() {\n        // Skip notification if display name is unknown (might depend on\n        // knowledge of members for groups).\n        return Boolean(this.displayName);\n    },\n    /** @param {string} body */\n    async post(body) {\n        if (this.model === \"discuss.channel\" && body.startsWith(\"/\")) {\n            const [firstWord] = body.substring(1).split(/\\s/);\n            const command = commandRegistry.get(firstWord, false);\n            if (\n                command &&\n                (!command.channel_types || command.channel_types.includes(this.channel_type))\n            ) {\n                await this.executeCommand(command, body);\n                return;\n            }\n        }\n        return super.post(...arguments);\n    },\n};\npatch(Thread.prototype, threadPatch);\n", "import { closeStream } from \"@mail/utils/common/misc\";\n\nimport { browser } from \"@web/core/browser/browser\";\n\nfunction drawAndBlurImageOnCanvas(image, blurAmount, canvas) {\n    canvas.width = image.width;\n    canvas.height = image.height;\n    if (blurAmount === 0) {\n        canvas.getContext(\"2d\").drawImage(image, 0, 0, image.width, image.height);\n        return;\n    }\n    canvas.getContext(\"2d\").clearRect(0, 0, image.width, image.height);\n    canvas.getContext(\"2d\").save();\n    // FIXME : Does not work on safari https://bugs.webkit.org/show_bug.cgi?id=198416\n    canvas.getContext(\"2d\").filter = `blur(${blurAmount}px)`;\n    canvas.getContext(\"2d\").drawImage(image, 0, 0, image.width, image.height);\n    canvas.getContext(\"2d\").restore();\n}\n\nexport class BlurManager {\n    canvas = document.createElement(\"canvas\");\n    canvasBlur = document.createElement(\"canvas\");\n    canvasMask = document.createElement(\"canvas\");\n    canvasStream;\n    isVideoDataLoaded = false;\n    rejectStreamPromise;\n    resolveStreamPromise;\n    selfieSegmentation = new window.SelfieSegmentation({\n        locateFile: (file) => {\n            return `https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation@0.1/${file}`;\n        },\n    });\n    /**\n     * Promise or undefined, based on the input stream, resolved when selfieSegmentation has started painting on the canvas,\n     * resolves into a web.MediaStream that is the blurred version of the input stream.\n     */\n    stream;\n    video = document.createElement(\"video\");\n\n    constructor(\n        stream,\n        { backgroundBlur = 10, edgeBlur = 10, modelSelection = 1, selfieMode = false } = {}\n    ) {\n        this.edgeBlur = edgeBlur;\n        this.backgroundBlur = backgroundBlur;\n        this._onVideoPlay = this._onVideoPlay.bind(this);\n        this.video.addEventListener(\"loadeddata\", this._onVideoPlay);\n        this.canvas.getContext(\"2d\"); // canvas.captureStream() doesn't work on firefox before getContext() is called.\n        this.canvasStream = this.canvas.captureStream();\n        let rejectStreamPromise;\n        let resolveStreamPromise;\n        Object.assign(this, {\n            stream: new Promise((resolve, reject) => {\n                rejectStreamPromise = reject;\n                resolveStreamPromise = resolve;\n            }),\n            rejectStreamPromise,\n            resolveStreamPromise,\n        });\n        this.video.srcObject = stream;\n        this.video.load();\n        this.selfieSegmentation.setOptions({\n            selfieMode,\n            modelSelection,\n        });\n        this.selfieSegmentation.onResults((r) => this._onSelfieSegmentationResults(r));\n        this.video.autoplay = true;\n        Promise.resolve(this.video.play()).catch(() => {});\n    }\n\n    close() {\n        this.video.removeEventListener(\"loadeddata\", this._onVideoPlay);\n        this.video.srcObject = null;\n        this.isVideoDataLoaded = false;\n        this.selfieSegmentation.reset();\n        closeStream(this.canvasStream);\n        this.canvasStream = null;\n        if (this.rejectStreamPromise) {\n            this.rejectStreamPromise(\n                new Error(\"The source stream was removed before the beginning of the blur process\")\n            );\n        }\n    }\n\n    _drawWithCompositing(image, compositeOperation) {\n        this.canvas.getContext(\"2d\").globalCompositeOperation = compositeOperation;\n        this.canvas.getContext(\"2d\").drawImage(image, 0, 0);\n    }\n\n    /**\n     * @private\n     */\n    _onVideoPlay() {\n        this.isVideoDataLoaded = true;\n        this._requestFrame();\n    }\n\n    /**\n     * @private\n     */\n    async _onFrame() {\n        if (!this.selfieSegmentation) {\n            return;\n        }\n        if (!this.video) {\n            return;\n        }\n        if (!this.isVideoDataLoaded) {\n            return;\n        }\n        await this.selfieSegmentation.send({ image: this.video });\n        browser.setTimeout(() => this._requestFrame(), Math.floor(1000 / 30)); // 30 fps\n    }\n\n    /**\n     * @private\n     */\n    _onSelfieSegmentationResults(results) {\n        drawAndBlurImageOnCanvas(results.image, this.backgroundBlur, this.canvasBlur);\n        this.canvas.width = this.canvasBlur.width;\n        this.canvas.height = this.canvasBlur.height;\n        drawAndBlurImageOnCanvas(results.segmentationMask, this.edgeBlur, this.canvasMask);\n        this.canvas.getContext(\"2d\").save();\n        this.canvas\n            .getContext(\"2d\")\n            .drawImage(results.image, 0, 0, this.canvas.width, this.canvas.height);\n        this._drawWithCompositing(this.canvasMask, \"destination-in\");\n        this._drawWithCompositing(this.canvasBlur, \"destination-over\");\n        this.canvas.getContext(\"2d\").restore();\n    }\n\n    /**\n     * @private\n     */\n    _requestFrame() {\n        browser.requestAnimationFrame(async () => {\n            await this._onFrame();\n            this.resolveStreamPromise(this.canvasStream);\n        });\n    }\n}\n", "import { CallActionList } from \"@mail/discuss/call/common/call_action_list\";\nimport { CallParticipantCard } from \"@mail/discuss/call/common/call_participant_card\";\nimport { PttAdBanner } from \"@mail/discuss/call/common/ptt_ad_banner\";\nimport { isEventHandled, markEventHandled } from \"@web/core/utils/misc\";\n\nimport {\n    Component,\n    onMounted,\n    onPatched,\n    onWillUnmount,\n    useExternalListener,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef CardData\n * @property {string} key\n * @property {import(\"models\").RtcSession} session\n * @property {MediaStream} videoStream\n * @property {import(\"models\").ChannelMember} [member]\n */\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Thread} thread\n * @property {boolean} [compact]\n * @extends {Component<Props, Env>}\n */\nexport class Call extends Component {\n    static components = { CallActionList, CallParticipantCard, PttAdBanner };\n    static props = [\"thread\", \"compact?\"];\n    static template = \"discuss.Call\";\n\n    overlayTimeout;\n\n    setup() {\n        super.setup();\n        this.grid = useRef(\"grid\");\n        this.notification = useService(\"notification\");\n        this.rtc = useState(useService(\"discuss.rtc\"));\n        this.state = useState({\n            isFullscreen: false,\n            sidebar: false,\n            tileWidth: 0,\n            tileHeight: 0,\n            columnCount: 0,\n            overlay: false,\n            /** @type {CardData|undefined} */\n            insetCard: undefined,\n        });\n        this.store = useState(useService(\"mail.store\"));\n        onMounted(() => {\n            this.resizeObserver = new ResizeObserver(() => this.arrangeTiles());\n            this.resizeObserver.observe(this.grid.el);\n            this.arrangeTiles();\n        });\n        onPatched(() => this.arrangeTiles());\n        onWillUnmount(() => {\n            this.resizeObserver.disconnect();\n            browser.clearTimeout(this.overlayTimeout);\n        });\n        useExternalListener(browser, \"fullscreenchange\", this.onFullScreenChange);\n    }\n\n    get isActiveCall() {\n        return Boolean(this.props.thread.eq(this.rtc.state?.channel));\n    }\n\n    get minimized() {\n        if (this.state.isFullscreen || this.props.thread.activeRtcSession) {\n            return false;\n        }\n        if (!this.isActiveCall || this.props.thread.videoCount === 0 || this.props.compact) {\n            return true;\n        }\n        return false;\n    }\n\n    /** @returns {CardData[]} */\n    get visibleCards() {\n        const raisingHandCards = [];\n        const sessionCards = [];\n        const invitationCards = [];\n        const filterVideos = this.store.settings.showOnlyVideo && this.props.thread.videoCount > 0;\n        for (const session of this.props.thread.rtcSessions) {\n            const target = session.raisingHand ? raisingHandCards : sessionCards;\n            const cameraStream = session.isCameraOn\n                ? session.videoStreams.get(\"camera\")\n                : undefined;\n            if (!filterVideos || cameraStream) {\n                target.push({\n                    key: \"session_main_\" + session.id,\n                    session,\n                    type: \"camera\",\n                    videoStream: cameraStream,\n                });\n            }\n            const screenStream = session.isScreenSharingOn\n                ? session.videoStreams.get(\"screen\")\n                : undefined;\n            if (screenStream) {\n                target.push({\n                    key: \"session_secondary_\" + session.id,\n                    session,\n                    type: \"screen\",\n                    videoStream: screenStream,\n                });\n            }\n        }\n        if (!filterVideos) {\n            for (const member of this.props.thread.invitedMembers) {\n                invitationCards.push({\n                    key: \"member_\" + member.id,\n                    member,\n                });\n            }\n        }\n        raisingHandCards.sort((c1, c2) => {\n            return c1.session.raisingHand - c2.session.raisingHand;\n        });\n        sessionCards.sort((c1, c2) => {\n            return (\n                c1.session.channelMember?.persona?.name?.localeCompare(\n                    c2.session.channelMember?.persona?.name\n                ) ?? 1\n            );\n        });\n        invitationCards.sort((c1, c2) => {\n            return c1.member.persona?.name?.localeCompare(c2.member.persona?.name) ?? 1;\n        });\n        return raisingHandCards.concat(sessionCards, invitationCards);\n    }\n\n    /** @returns {CardData[]} */\n    get visibleMainCards() {\n        const activeSession = this.props.thread.activeRtcSession;\n        this.state.insetCard = undefined;\n        if (!activeSession) {\n            return this.visibleCards;\n        }\n        const type = activeSession.mainVideoStreamType;\n        if (type === \"screen\" || activeSession.isScreenSharingOn) {\n            this.setInset(activeSession, type === \"camera\" ? \"screen\" : \"camera\");\n        }\n        return [\n            {\n                key: \"session_\" + activeSession.id,\n                session: activeSession,\n                type,\n                videoStream: activeSession.getStream(type),\n            },\n        ];\n    }\n\n    /**\n     * @param {RtcSession} session\n     * @param {String} [videoType]\n     */\n    setInset(session, videoType) {\n        this.state.insetCard = {\n            key: \"session_\" + session.id,\n            session,\n            type: videoType,\n            videoStream: session.getStream(videoType),\n        };\n    }\n\n    get hasCallNotifications() {\n        return Boolean(\n            (!this.props.compact || this.state.isFullscreen) &&\n                this.isActiveCall &&\n                this.rtc.notifications.size\n        );\n    }\n\n    get hasSidebarButton() {\n        return Boolean(\n            this.props.thread.activeRtcSession && this.state.overlay && !this.props.compact\n        );\n    }\n\n    get isControllerFloating() {\n        return (\n            this.state.isFullscreen || (this.props.thread.activeRtcSession && !this.props.compact)\n        );\n    }\n\n    onMouseleaveMain(ev) {\n        if (ev.relatedTarget && ev.relatedTarget.closest(\".o-dropdown--menu\")) {\n            // the overlay should not be hidden when the cursor leaves to enter the controller dropdown\n            return;\n        }\n        this.state.overlay = false;\n    }\n\n    onMousemoveMain(ev) {\n        if (isEventHandled(ev, \"CallMain.MousemoveOverlay\")) {\n            return;\n        }\n        this.showOverlay();\n    }\n\n    onMousemoveOverlay(ev) {\n        markEventHandled(ev, \"CallMain.MousemoveOverlay\");\n        this.state.overlay = true;\n        browser.clearTimeout(this.overlayTimeout);\n    }\n\n    showOverlay() {\n        this.state.overlay = true;\n        browser.clearTimeout(this.overlayTimeout);\n        this.overlayTimeout = browser.setTimeout(() => {\n            this.state.overlay = false;\n        }, 3000);\n    }\n\n    arrangeTiles() {\n        if (!this.grid.el) {\n            return;\n        }\n        const { width, height } = this.grid.el.getBoundingClientRect();\n        const aspectRatio = this.minimized ? 1 : 16 / 9;\n        const tileCount = this.grid.el.children.length;\n        let optimal = {\n            area: 0,\n            columnCount: 0,\n            tileHeight: 0,\n            tileWidth: 0,\n        };\n        for (let columnCount = 1; columnCount <= tileCount; columnCount++) {\n            const rowCount = Math.ceil(tileCount / columnCount);\n            const potentialHeight = width / (columnCount * aspectRatio);\n            const potentialWidth = height / rowCount;\n            let tileHeight;\n            let tileWidth;\n            if (potentialHeight > potentialWidth) {\n                tileHeight = Math.floor(potentialWidth);\n                tileWidth = Math.floor(tileHeight * aspectRatio);\n            } else {\n                tileWidth = Math.floor(width / columnCount);\n                tileHeight = Math.floor(tileWidth / aspectRatio);\n            }\n            const area = tileHeight * tileWidth;\n            if (area <= optimal.area) {\n                continue;\n            }\n            optimal = {\n                area,\n                columnCount,\n                tileHeight,\n                tileWidth,\n            };\n        }\n        Object.assign(this.state, {\n            tileWidth: optimal.tileWidth,\n            tileHeight: optimal.tileHeight,\n            columnCount: optimal.columnCount,\n        });\n    }\n\n    async enterFullScreen() {\n        const el = document.body;\n        try {\n            if (el.requestFullscreen) {\n                await el.requestFullscreen();\n            } else if (el.mozRequestFullScreen) {\n                await el.mozRequestFullScreen();\n            } else if (el.webkitRequestFullscreen) {\n                await el.webkitRequestFullscreen();\n            }\n            this.state.isFullscreen = true;\n        } catch {\n            this.state.isFullscreen = false;\n            this.notification.add(_t(\"The Fullscreen mode was denied by the browser\"), {\n                type: \"warning\",\n            });\n        }\n    }\n\n    async exitFullScreen() {\n        const fullscreenElement = document.webkitFullscreenElement || document.fullscreenElement;\n        if (fullscreenElement) {\n            if (document.exitFullscreen) {\n                await document.exitFullscreen();\n            } else if (document.mozCancelFullScreen) {\n                await document.mozCancelFullScreen();\n            } else if (document.webkitCancelFullScreen) {\n                await document.webkitCancelFullScreen();\n            }\n        }\n        this.state.isFullscreen = false;\n    }\n\n    /**\n     * @private\n     */\n    onFullScreenChange() {\n        this.state.isFullscreen = Boolean(\n            document.webkitFullscreenElement || document.fullscreenElement\n        );\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\n\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useCallActions } from \"./call_actions\";\n\nexport class CallActionList extends Component {\n    static components = { Dropdown, DropdownItem };\n    static props = [\"thread\", \"fullscreen\", \"compact?\"];\n    static template = \"discuss.CallActionList\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.rtc = useState(useService(\"discuss.rtc\"));\n        this.callActions = useCallActions();\n    }\n\n    get MORE() {\n        return _t(\"More\");\n    }\n\n    get isOfActiveCall() {\n        return Boolean(this.props.thread.eq(this.rtc.state?.channel));\n    }\n\n    get isSmall() {\n        return Boolean(this.props.compact && !this.props.fullscreen.isActive);\n    }\n\n    get isMobileOS() {\n        return isMobileOS();\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    async onClickRejectCall(ev) {\n        if (this.rtc.state.hasPendingRequest) {\n            return;\n        }\n        await this.rtc.leaveCall(this.props.thread);\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    async onClickToggleAudioCall(ev) {\n        await this.rtc.toggleCall(this.props.thread);\n    }\n}\n", "import { useComponent, useState } from \"@odoo/owl\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nexport const callActionsRegistry = registry.category(\"discuss.call/actions\");\n\ncallActionsRegistry\n    .add(\"mute\", {\n        condition: (component) => component.rtc,\n        name: (component) => (component.rtc.selfSession.isMute ? _t(\"Unmute\") : _t(\"Mute\")),\n        isActive: (component) => component.rtc.selfSession?.isMute,\n        inactiveIcon: \"fa-microphone\",\n        icon: \"fa-microphone-slash\",\n        activeClass: \"text-danger\",\n        select: (component) => {\n            if (component.rtc.selfSession.isMute) {\n                if (component.rtc.selfSession.isSelfMuted) {\n                    component.rtc.unmute();\n                }\n                if (component.rtc.selfSession.isDeaf) {\n                    component.rtc.undeafen();\n                }\n            } else {\n                component.rtc.mute();\n            }\n        },\n        sequence: 10,\n    })\n    .add(\"deafen\", {\n        condition: (component) => component.rtc,\n        name: (component) => (component.rtc.selfSession.isDeaf ? _t(\"Undeafen\") : _t(\"Deafen\")),\n        isActive: (component) => component.rtc.selfSession?.isDeaf,\n        inactiveIcon: \"fa-headphones\",\n        icon: \"fa-deaf\",\n        activeClass: \"text-danger\",\n        select: (component) =>\n            component.rtc.selfSession.isDeaf ? component.rtc.undeafen() : component.rtc.deafen(),\n        sequence: 20,\n    })\n    .add(\"camera-on\", {\n        condition: (component) => component.rtc,\n        name: (component) =>\n            component.rtc.selfSession.isCameraOn ? _t(\"Stop camera\") : _t(\"Turn camera on\"),\n        isActive: (component) => component.rtc.selfSession?.isCameraOn,\n        icon: \"fa-video-camera\",\n        activeClass: \"text-success\",\n        select: (component) => component.rtc.toggleVideo(\"camera\"),\n        sequence: 30,\n    })\n    .add(\"raise-hand\", {\n        condition: (component) => component.rtc,\n        name: (component) =>\n            component.rtc.selfSession.raisingHand ? _t(\"Lower Hand\") : _t(\"Raise Hand\"),\n        isActive: (component) => component.rtc.selfSession?.raisingHand,\n        icon: \"fa-hand-paper-o\",\n        select: (component) => component.rtc.raiseHand(!component.rtc.selfSession.raisingHand),\n        sequence: 40,\n    })\n    .add(\"share-screen\", {\n        condition: (component) => component.rtc && !isMobileOS(),\n        name: (component) =>\n            component.rtc.selfSession.isScreenSharingOn\n                ? _t(\"Stop Sharing Screen\")\n                : _t(\"Share Screen\"),\n        isActive: (component) => component.rtc.selfSession?.isScreenSharingOn,\n        icon: \"fa-desktop\",\n        select: (component) => component.rtc.toggleVideo(\"screen\"),\n        sequence: 50,\n    })\n    .add(\"fullscreen\", {\n        condition: (component) => component.props && component.props.fullscreen,\n        name: (component) =>\n            component.props.fullscreen.isActive ? _t(\"Exit Fullscreen\") : _t(\"Enter Full Screen\"),\n        isActive: (component) => component.props.fullscreen.isActive,\n        inactiveIcon: \"fa-arrows-alt\",\n        icon: \"fa-compress\",\n        select: (component) => {\n            if (component.props.fullscreen.isActive) {\n                component.props.fullscreen.exit();\n            } else {\n                component.props.fullscreen.enter();\n            }\n        },\n        sequence: 60,\n    });\n\nfunction transformAction(component, id, action) {\n    return {\n        id,\n        /** Condition to display this action */\n        get condition() {\n            return action.condition(component);\n        },\n        /** Name of this action, displayed to the user */\n        get name() {\n            return typeof action.name === \"function\" ? action.name(component) : action.name;\n        },\n        get isActive() {\n            return action.isActive(component);\n        },\n        inactiveIcon: action.inactiveIcon,\n        /** Icon for the button of this action */\n        get icon() {\n            return typeof action.icon === \"function\" ? action.icon(component) : action.icon;\n        },\n        activeClass: action.activeClass,\n        /**  Action to execute when this action is selected */\n        select() {\n            action.select(component);\n        },\n        /** Determines the order of this action (smaller first) */\n        get sequence() {\n            return typeof action.sequence === \"function\"\n                ? action.sequence(component)\n                : action.sequence;\n        },\n    };\n}\n\nexport function useCallActions() {\n    const component = useComponent();\n    const state = useState({ actions: [] });\n    state.actions = callActionsRegistry\n        .getEntries()\n        .map(([id, action]) => transformAction(component, id, action));\n    return {\n        get actions() {\n            return state.actions\n                .filter((action) => action.condition)\n                .sort((a1, a2) => a1.sequence - a2.sequence);\n        },\n    };\n}\n", "import { Component, onMounted, onWillUnmount, useState } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { CONNECTION_TYPES } from \"@mail/discuss/call/common/rtc_service\";\n\nconst PROTOCOLS_TEXT = { host: \"HOST\", srflx: \"STUN\", prflx: \"STUN\", relay: \"TURN\" };\n\nexport class CallContextMenu extends Component {\n    static props = [\"rtcSession\", \"close?\"];\n    static template = \"discuss.CallContextMenu\";\n\n    updateStatsTimeout;\n    rtcConnectionTypes = CONNECTION_TYPES;\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.rtc = useState(useService(\"discuss.rtc\"));\n        this.state = useState({\n            downloadStats: {},\n            uploadStats: {},\n            producerStats: {},\n            peerStats: {},\n        });\n        onMounted(() => {\n            if (!this.env.debug) {\n                return;\n            }\n            this.updateStats();\n            this.updateStatsTimeout = browser.setInterval(() => this.updateStats(), 3000);\n        });\n        onWillUnmount(() => browser.clearInterval(this.updateStatsTimeout));\n    }\n\n    get isSelf() {\n        return this.rtc.selfSession?.eq(this.props.rtcSession);\n    }\n\n    get inboundConnectionTypeText() {\n        const candidateType =\n            this.rtc.state.connectionType === CONNECTION_TYPES.SERVER\n                ? this.state.downloadStats.remoteCandidateType\n                : this.state.peerStats.remoteCandidateType;\n        return this.formatProtocol(candidateType);\n    }\n\n    get outboundConnectionTypeText() {\n        const candidateType =\n            this.rtc.state.connectionType === CONNECTION_TYPES.SERVER\n                ? this.state.uploadStats.localCandidateType\n                : this.state.peerStats.localCandidateType;\n        return this.formatProtocol(candidateType);\n    }\n\n    get volume() {\n        return this.store.settings.getVolume(this.props.rtcSession);\n    }\n\n    /**\n     * @param {string} candidateType\n     * @returns {string} a formatted string that describes the connection type e.g: \"prflx (STUN)\"\n     */\n    formatProtocol(candidateType) {\n        if (!candidateType) {\n            return _t(\"no connection\");\n        }\n        return _t(\"%(candidateType)s (%(protocol)s)\", {\n            candidateType,\n            protocol: PROTOCOLS_TEXT[candidateType],\n        });\n    }\n\n    async updateStats() {\n        if (this.rtc.selfSession?.eq(this.props.rtcSession)) {\n            if (this.rtc.sfuClient) {\n                const { uploadStats, downloadStats, ...producerStats } =\n                    await this.rtc.sfuClient.getStats();\n                const formattedUploadStats = {};\n                for (const value of uploadStats.values?.() || []) {\n                    switch (value.type) {\n                        case \"candidate-pair\":\n                            if (value.state === \"succeeded\" && value.localCandidateId) {\n                                formattedUploadStats.localCandidateType =\n                                    uploadStats.get(value.localCandidateId)?.candidateType || \"\";\n                                formattedUploadStats.availableOutgoingBitrate =\n                                    value.availableOutgoingBitrate;\n                            }\n                            break;\n                        case \"transport\":\n                            formattedUploadStats.dtlsState = value.dtlsState;\n                            formattedUploadStats.iceState = value.iceState;\n                            formattedUploadStats.packetsSent = value.packetsSent;\n                            break;\n                    }\n                }\n                const formattedDownloadStats = {};\n                for (const value of downloadStats.values?.() || []) {\n                    switch (value.type) {\n                        case \"candidate-pair\":\n                            if (value.state === \"succeeded\" && value.localCandidateId) {\n                                formattedDownloadStats.remoteCandidateType =\n                                    downloadStats.get(value.remoteCandidateId)?.candidateType || \"\";\n                            }\n                            break;\n                        case \"transport\":\n                            formattedDownloadStats.dtlsState = value.dtlsState;\n                            formattedDownloadStats.iceState = value.iceState;\n                            formattedDownloadStats.packetsReceived = value.packetsReceived;\n                            break;\n                    }\n                }\n                const formattedProducerStats = {};\n                for (const [type, stat] of Object.entries(producerStats)) {\n                    const currentTypeStats = {};\n                    for (const value of stat.values()) {\n                        switch (value.type) {\n                            case \"codec\":\n                                currentTypeStats.codec = value.mimeType;\n                                currentTypeStats.clockRate = value.clockRate;\n                                break;\n                        }\n                    }\n                    formattedProducerStats[type] = currentTypeStats;\n                }\n                this.state.uploadStats = formattedUploadStats;\n                this.state.downloadStats = formattedDownloadStats;\n                this.state.producerStats = formattedProducerStats;\n            }\n            return;\n        }\n        this.state.peerStats = await this.rtc.p2pService.getFormattedStats(\n            this.props.rtcSession.id\n        );\n    }\n\n    onChangeVolume(ev) {\n        const volume = Number(ev.target.value);\n        this.store.settings.saveVolumeSetting({\n            guestId: this.props.rtcSession?.guestId,\n            partnerId: this.props.rtcSession?.partnerId,\n            volume,\n        });\n        this.props.rtcSession.volume = volume;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class CallInvitation extends Component {\n    static props = [\"thread\"];\n    static template = \"discuss.CallInvitation\";\n\n    setup() {\n        super.setup();\n        this.rtc = useService(\"discuss.rtc\");\n    }\n\n    async onClickAccept(ev) {\n        this.props.thread.open();\n        if (this.rtc.state.hasPendingRequest) {\n            return;\n        }\n        await this.rtc.toggleCall(this.props.thread);\n    }\n\n    onClickAvatar(ev) {\n        this.props.thread.open();\n    }\n\n    onClickRefuse(ev) {\n        if (this.rtc.state.hasPendingRequest) {\n            return;\n        }\n        this.rtc.leaveCall(this.props.thread);\n    }\n}\n", "import { CallInvitation } from \"@mail/discuss/call/common/call_invitation\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class CallInvitations extends Component {\n    static props = [];\n    static components = { CallInvitation };\n    static template = \"discuss.CallInvitations\";\n\n    setup() {\n        super.setup();\n        this.rtc = useState(useService(\"discuss.rtc\"));\n        this.store = useState(useService(\"mail.store\"));\n    }\n}\n\nregistry.category(\"main_components\").add(\"discuss.CallInvitations\", { Component: CallInvitations });\n", "import { Component, useState } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { callActionsRegistry, useCallActions } from \"./call_actions\";\n\nexport class CallMenu extends Component {\n    static props = [];\n    static template = \"discuss.CallMenu\";\n    setup() {\n        super.setup();\n        this.rtc = useState(useService(\"discuss.rtc\"));\n        this.callActions = useCallActions();\n    }\n\n    get icon() {\n        return (\n            callActionsRegistry.get(this.rtc.lastSelfCallAction, undefined)?.icon ?? \"fa-microphone\"\n        );\n    }\n}\n\nregistry.category(\"systray\").add(\"discuss.CallMenu\", { Component: CallMenu }, { sequence: 100 });\n", "import { CallContextMenu } from \"@mail/discuss/call/common/call_context_menu\";\nimport { CallParticipantVideo } from \"@mail/discuss/call/common/call_participant_video\";\nimport { CONNECTION_TYPES } from \"@mail/discuss/call/common/rtc_service\";\nimport { useHover } from \"@mail/utils/common/hooks\";\nimport { isEventHandled, markEventHandled } from \"@web/core/utils/misc\";\nimport { browser } from \"@web/core/browser/browser\";\n\nimport {\n    Component,\n    onMounted,\n    onWillUnmount,\n    useRef,\n    useState,\n    useExternalListener,\n} from \"@odoo/owl\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nconst HIDDEN_CONNECTION_STATES = new Set([undefined, \"connected\", \"completed\"]);\n\nexport class CallParticipantCard extends Component {\n    static props = [\"className\", \"cardData\", \"thread\", \"minimized?\", \"inset?\"];\n    static components = { CallParticipantVideo };\n    static template = \"discuss.CallParticipantCard\";\n\n    setup() {\n        super.setup();\n        this.contextMenuAnchorRef = useRef(\"contextMenuAnchor\");\n        this.root = useRef(\"root\");\n        this.popover = usePopover(CallContextMenu);\n        this.rtc = useState(useService(\"discuss.rtc\"));\n        this.store = useState(useService(\"mail.store\"));\n        this.ui = useState(useService(\"ui\"));\n        this.rootHover = useHover(\"root\");\n        this.state = useState({ drag: false, dragPos: undefined });\n        onMounted(() => {\n            if (!this.rtcSession) {\n                return;\n            }\n            this.rtc.updateVideoDownload(this.rtcSession, {\n                viewCountIncrement: 1,\n            });\n        });\n        onWillUnmount(() => {\n            if (!this.rtcSession) {\n                return;\n            }\n            this.rtc.updateVideoDownload(this.rtcSession, {\n                viewCountIncrement: -1,\n            });\n        });\n        useExternalListener(browser, \"fullscreenchange\", this.onFullScreenChange);\n    }\n\n    get isContextMenuAvailable() {\n        if (!this.rtcSession) {\n            return false;\n        }\n        return (\n            !this.rtcSession.eq(this.rtc.selfSession) ||\n            (this.env.debug && this.rtc.state.connectionType === CONNECTION_TYPES.SERVER)\n        );\n    }\n\n    get rtcSession() {\n        return this.props.cardData.session;\n    }\n\n    get channelMember() {\n        return this.rtcSession ? this.rtcSession.channelMember : this.props.cardData.member;\n    }\n\n    get isOfActiveCall() {\n        return Boolean(this.rtcSession && this.rtcSession.channel?.eq(this.rtc.state.channel));\n    }\n\n    get showConnectionState() {\n        return Boolean(\n            this.isOfActiveCall && !HIDDEN_CONNECTION_STATES.has(this.rtcSession.connectionState)\n        );\n    }\n\n    get showServerState() {\n        return Boolean(\n            this.rtcSession.channelMember?.persona.eq(this.store.self) &&\n                this.rtc.state.serverState &&\n                this.rtc.state.serverState !== \"connected\"\n        );\n    }\n\n    get name() {\n        return this.channelMember?.persona.name;\n    }\n\n    get hasMediaError() {\n        return (\n            this.isOfActiveCall &&\n            Boolean(this.rtcSession?.videoError || this.rtcSession?.audioError)\n        );\n    }\n\n    get hasVideo() {\n        return Boolean(this.props.cardData.videoStream);\n    }\n\n    get isTalking() {\n        return Boolean(this.rtcSession && this.rtcSession.isActuallyTalking);\n    }\n\n    get hasRaisingHand() {\n        const screenStream = this.rtcSession.videoStreams.get(\"screen\");\n        return Boolean(\n            this.rtcSession.raisingHand &&\n                (!screenStream || screenStream !== this.props.cardData.videoStream)\n        );\n    }\n\n    async onClick(ev) {\n        if (isEventHandled(ev, \"CallParticipantCard.clickVolumeAnchor\")) {\n            return;\n        }\n        if (this.state.drag) {\n            this.state.drag = false;\n            return;\n        }\n        if (this.rtcSession) {\n            const channel = this.rtcSession.channel;\n            this.rtcSession.mainVideoStreamType = this.props.cardData.type;\n            if (this.rtcSession.eq(channel.activeRtcSession) && !this.props.inset) {\n                channel.activeRtcSession = undefined;\n                this.rtcSession.mainVideoStreamType = undefined;\n            } else {\n                const activeRtcSession = channel.activeRtcSession;\n                const currentMainVideoType = this.rtcSession.mainVideoStreamType;\n                channel.activeRtcSession = this.rtcSession;\n                if (this.props.inset && activeRtcSession) {\n                    this.props.inset(activeRtcSession, currentMainVideoType);\n                }\n            }\n            return;\n        }\n        await rpc(\"/mail/rtc/channel/cancel_call_invitation\", {\n            channel_id: this.props.thread.id,\n            member_ids: [this.channelMember.id],\n        });\n    }\n\n    async onClickReplay() {\n        this.env.bus.trigger(\"RTC-SERVICE:PLAY_MEDIA\");\n    }\n\n    /**\n     * @param {Event} ev\n     */\n    onContextMenu(ev) {\n        ev.preventDefault();\n        markEventHandled(ev, \"CallParticipantCard.clickVolumeAnchor\");\n        if (this.popover.isOpen) {\n            this.popover.close();\n            return;\n        }\n        if (!this.contextMenuAnchorRef?.el) {\n            return;\n        }\n        this.popover.open(this.contextMenuAnchorRef.el, {\n            rtcSession: this.rtcSession,\n        });\n    }\n\n    onMouseDown() {\n        if (!this.props.inset) {\n            return;\n        }\n        const onMousemove = (ev) => this.drag(ev);\n        const onMouseup = () => {\n            const insetEl = this.root.el;\n            if (parseInt(insetEl.style.left) < insetEl.parentNode.offsetWidth / 2) {\n                insetEl.style.left = \"1vh\";\n                insetEl.style.right = \"\";\n            } else {\n                insetEl.style.left = \"\";\n                insetEl.style.right = \"1vh\";\n            }\n            if (parseInt(insetEl.style.top) < insetEl.parentNode.offsetHeight / 2) {\n                insetEl.style.top = \"1vh\";\n                insetEl.style.bottom = \"\";\n            } else {\n                insetEl.style.bottom = \"1vh\";\n                insetEl.style.top = \"\";\n            }\n            document.removeEventListener(\"mouseup\", onMouseup);\n            document.removeEventListener(\"mousemove\", onMousemove);\n        };\n        document.addEventListener(\"mouseup\", onMouseup);\n        document.addEventListener(\"mousemove\", onMousemove);\n    }\n\n    drag(ev) {\n        this.state.drag = true;\n        const insetEl = this.root.el;\n        const parent = insetEl.parentNode;\n        const clientX = ev.clientX ?? ev.touches[0].clientX;\n        const clientY = ev.clientY ?? ev.touches[0].clientY;\n        if (!this.state.dragPos) {\n            this.state.dragPos = { posX: clientX, posY: clientY };\n        }\n        const dX = this.state.dragPos.posX - clientX;\n        const dY = this.state.dragPos.posY - clientY;\n        this.state.dragPos.posX = Math.min(\n            Math.max(clientX, parent.offsetLeft),\n            parent.offsetLeft + parent.offsetWidth - insetEl.clientWidth\n        );\n        this.state.dragPos.posY = Math.min(\n            Math.max(clientY, parent.offsetTop),\n            parent.offsetTop + parent.offsetHeight - insetEl.clientHeight\n        );\n        insetEl.style.left =\n            Math.min(\n                Math.max(insetEl.offsetLeft - dX, 0),\n                parent.offsetWidth - insetEl.clientWidth\n            ) + \"px\";\n        insetEl.style.top =\n            Math.min(\n                Math.max(insetEl.offsetTop - dY, 0),\n                parent.offsetHeight - insetEl.clientHeight\n            ) + \"px\";\n    }\n\n    onFullScreenChange() {\n        this.root.el.style = \"left:''; top:''\";\n    }\n}\n", "import { Component, onMounted, onPatched, useExternalListener, useRef, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").RtcSession} session\n * @extends {Component<Props, Env>}\n */\nexport class CallParticipantVideo extends Component {\n    static props = [\"session\", \"type\", \"inset?\"];\n    static template = \"discuss.CallParticipantVideo\";\n\n    setup() {\n        super.setup();\n        this.rtc = useState(useService(\"discuss.rtc\"));\n        this.root = useRef(\"root\");\n        onMounted(() => this._update());\n        onPatched(() => this._update());\n        useExternalListener(this.env.bus, \"RTC-SERVICE:PLAY_MEDIA\", async () => {\n            await this.play();\n        });\n    }\n\n    _update() {\n        if (!this.root.el) {\n            return;\n        }\n        if (!this.props.session || !this.props.session.getStream(this.props.type)) {\n            this.root.el.srcObject = undefined;\n        } else {\n            this.root.el.srcObject = this.props.session.getStream(this.props.type);\n        }\n        this.root.el.load();\n    }\n\n    async play() {\n        try {\n            await this.root.el?.play?.();\n            this.props.session.videoError = undefined;\n        } catch (error) {\n            this.props.session.videoError = error.name;\n        }\n    }\n\n    async onVideoLoadedMetaData() {\n        await this.play();\n    }\n}\n", "import { Component, onWillStart, useExternalListener, useState } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\n\nexport class CallSettings extends Component {\n    static template = \"discuss.CallSettings\";\n    static props = [\"withActionPanel?\", \"*\"];\n    static defaultProps = {\n        withActionPanel: true,\n    };\n    static components = { ActionPanel };\n\n    setup() {\n        super.setup();\n        this.notification = useService(\"notification\");\n        this.store = useState(useService(\"mail.store\"));\n        this.rtc = useState(useService(\"discuss.rtc\"));\n        this.state = useState({\n            userDevices: [],\n        });\n        this.pttExtService = useState(useService(\"discuss.ptt_extension\"));\n        this.saveBackgroundBlurAmount = debounce(() => {\n            browser.localStorage.setItem(\n                \"mail_user_setting_background_blur_amount\",\n                this.store.settings.backgroundBlurAmount.toString()\n            );\n        }, 2000);\n        this.saveEdgeBlurAmount = debounce(() => {\n            browser.localStorage.setItem(\n                \"mail_user_setting_edge_blur_amount\",\n                this.store.settings.edgeBlurAmount.toString()\n            );\n        }, 2000);\n        useExternalListener(browser, \"keydown\", this._onKeyDown, { capture: true });\n        useExternalListener(browser, \"keyup\", this._onKeyUp, { capture: true });\n        onWillStart(async () => {\n            if (!browser.navigator.mediaDevices) {\n                // zxing-js: isMediaDevicesSuported or canEnumerateDevices is false.\n                this.notification.add(\n                    _t(\"Media devices unobtainable. SSL might not be set up properly.\"),\n                    { type: \"warning\" }\n                );\n                console.warn(\"Media devices unobtainable. SSL might not be set up properly.\");\n                return;\n            }\n            this.state.userDevices = await browser.navigator.mediaDevices.enumerateDevices();\n        });\n    }\n\n    get pushToTalkKeyText() {\n        const { shiftKey, ctrlKey, altKey, key } = this.store.settings.pushToTalkKeyFormat();\n        const f = (k, name) => (k ? name : \"\");\n        const keys = [f(ctrlKey, \"Ctrl\"), f(altKey, \"Alt\"), f(shiftKey, \"Shift\"), key].filter(\n            Boolean\n        );\n        return keys.join(\" + \");\n    }\n\n    get isMobileOS() {\n        return isMobileOS();\n    }\n\n    _onKeyDown(ev) {\n        if (!this.store.settings.isRegisteringKey) {\n            return;\n        }\n        ev.stopPropagation();\n        ev.preventDefault();\n        this.store.settings.setPushToTalkKey(ev);\n    }\n\n    _onKeyUp(ev) {\n        if (!this.store.settings.isRegisteringKey) {\n            return;\n        }\n        ev.stopPropagation();\n        ev.preventDefault();\n        this.store.settings.isRegisteringKey = false;\n    }\n\n    onChangeLogRtc(ev) {\n        this.store.settings.logRtc = ev.target.checked;\n    }\n\n    onChangeSelectAudioInput(ev) {\n        this.store.settings.setAudioInputDevice(ev.target.value);\n    }\n\n    onClickDownloadLogs() {\n        const data = JSON.stringify(Object.fromEntries(this.rtc.state.logs));\n        const blob = new Blob([data], { type: \"application/json\" });\n        const downloadLink = document.createElement(\"a\");\n        const channelId = this.rtc.state.logs.get(\"channelId\");\n        const sessionId = this.rtc.state.logs.get(\"selfSessionId\");\n        const now = luxon.DateTime.now().toFormat(\"yyyy-ll-dd_HH-mm\");\n        downloadLink.download = `RtcLogs_Channel_${channelId}_Session_${sessionId}_${now}.json`;\n        const url = URL.createObjectURL(blob);\n        downloadLink.href = url;\n        downloadLink.click();\n        URL.revokeObjectURL(url);\n    }\n\n    onClickRegisterKeyButton() {\n        this.store.settings.isRegisteringKey = !this.store.settings.isRegisteringKey;\n    }\n\n    onChangeDelay(ev) {\n        this.store.settings.setDelayValue(ev.target.value);\n    }\n\n    onChangeThreshold(ev) {\n        this.store.settings.setThresholdValue(parseFloat(ev.target.value));\n    }\n\n    onChangeBlur(ev) {\n        this.store.settings.useBlur = ev.target.checked;\n        browser.localStorage.setItem(\"mail_user_setting_use_blur\", this.store.settings.useBlur);\n    }\n\n    onChangeShowOnlyVideo(ev) {\n        const showOnlyVideo = ev.target.checked;\n        this.store.settings.showOnlyVideo = showOnlyVideo;\n        browser.localStorage.setItem(\n            \"mail_user_setting_show_only_video\",\n            this.store.settings.showOnlyVideo\n        );\n        const activeRtcSessions = this.store.allActiveRtcSessions;\n        if (showOnlyVideo && activeRtcSessions) {\n            activeRtcSessions\n                .filter((rtcSession) => !rtcSession.videoStream)\n                .forEach((rtcSession) => {\n                    rtcSession.channel.activeRtcSession = undefined;\n                });\n        }\n    }\n\n    onChangeBackgroundBlurAmount(ev) {\n        this.store.settings.backgroundBlurAmount = Number(ev.target.value);\n        this.saveBackgroundBlurAmount();\n    }\n\n    onChangeEdgeBlurAmount(ev) {\n        this.store.settings.edgeBlurAmount = Number(ev.target.value);\n        this.saveEdgeBlurAmount();\n    }\n}\n", "import { ChannelMember } from \"@mail/core/common/channel_member_model\";\nimport { Record } from \"@mail/core/common/record\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").ChannelMember} */\nconst ChannelMemberPatch = {\n    setup() {\n        super.setup(...arguments);\n        this.rtcSession = Record.one(\"RtcSession\");\n    },\n};\npatch(ChannelMember.prototype, ChannelMemberPatch);\n", "import { ChatWindow } from \"@mail/core/common/chat_window\";\nimport { Call } from \"@mail/discuss/call/common/call\";\n\nObject.assign(ChatWindow.components, { Call });\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { CallSettings } from \"@mail/discuss/call/common/call_settings\";\n\nexport class DiscussCallSettingsClientAction extends Component {\n    static components = { CallSettings };\n    static props = [\"*\"];\n    static template = \"mail.DiscussCallSettingsClientAction\";\n}\n\nregistry\n    .category(\"actions\")\n    .add(\"mail.discuss_call_settings_action\", DiscussCallSettingsClientAction);\n", "import { registry } from \"@web/core/registry\";\nimport { PeerToPeer } from \"@mail/discuss/call/common/peer_to_peer\";\n\nexport const discussP2P = {\n    dependencies: [\"bus_service\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    start(env, services) {\n        const p2p = new PeerToPeer({ notificationRoute: \"/mail/rtc/session/notify_call_members\" });\n        services[\"bus_service\"].subscribe(\n            \"discuss.channel.rtc.session/peer_notification\",\n            ({ sender, notifications }) => {\n                for (const content of notifications) {\n                    p2p.handleNotification(sender, content);\n                }\n            }\n        );\n        return p2p;\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.p2p\", discussP2P);\n", "// Broad human voice range of frequencies in hz.\nconst HUMAN_VOICE_FREQUENCY_RANGE = [80, 1000];\n\n//------------------------------------------------------------------------------\n// Public\n//------------------------------------------------------------------------------\n\n/**\n * monitors the activity of an audio mediaStreamTrack\n *\n * @param {MediaStreamTrack} track\n * @param {Object} [processorOptions] options for the audio processor\n * @param {Array<number>} [processorOptions.frequencyRange] the range of frequencies to monitor in hz\n * @param {number} [processorOptions.minimumActiveCycles] how many cycles have to pass since the\n *          last time the threshold was exceeded to go back to inactive state, this prevents\n *          stuttering when the speech volume oscillates around the threshold value.\n * @param {function(boolean):void} [processorOptions.onThreshold] a function to be called when the threshold is passed\n * @param {function(number):void} [processorOptions.onTic] a function to be called at each tics\n * @param {number} [processorOptions.volumeThreshold] the normalized minimum value for audio detection\n * @returns {Object} returnValue\n * @returns {function} returnValue.disconnect callback to cleanly end the monitoring\n */\nexport async function monitorAudio(track, processorOptions) {\n    // cloning the track so it is not affected by the enabled change of the original track.\n    const monitoredTrack = track.clone();\n    monitoredTrack.enabled = true;\n    const stream = new window.MediaStream([monitoredTrack]);\n    const AudioContext = window.AudioContext || window.webkitAudioContext;\n    if (!AudioContext) {\n        throw \"missing audio context\";\n    }\n    const audioContext = new AudioContext();\n    const source = audioContext.createMediaStreamSource(stream);\n\n    let processor;\n    try {\n        processor = await _loadAudioWorkletProcessor(source, audioContext, processorOptions);\n    } catch {\n        // In case Worklets are not supported by the browser (eg: Safari)\n        processor = _loadScriptProcessor(source, audioContext, processorOptions);\n    }\n\n    return async () => {\n        processor.disconnect();\n        source.disconnect();\n        monitoredTrack.stop();\n        try {\n            await audioContext.close();\n        } catch (e) {\n            if (e.name === \"InvalidStateError\") {\n                return; // the audio context is already closed\n            }\n            throw e;\n        }\n    };\n}\n\n//------------------------------------------------------------------------------\n// Private\n//------------------------------------------------------------------------------\n\n/**\n * @param {MediaStreamSource} source\n * @param {AudioContext} audioContext\n * @param {Object} [param2] options\n * @returns {Object} returnValue\n * @returns {function} returnValue.disconnect disconnect callback\n */\nfunction _loadScriptProcessor(\n    source,\n    audioContext,\n    {\n        frequencyRange = HUMAN_VOICE_FREQUENCY_RANGE,\n        minimumActiveCycles = 30,\n        onThreshold,\n        onTic,\n        volumeThreshold = 0.3,\n    } = {}\n) {\n    // audio setup\n    const bitSize = 1024;\n    const analyser = audioContext.createAnalyser();\n    source.connect(analyser);\n    const scriptProcessorNode = audioContext.createScriptProcessor(bitSize, 1, 1);\n    analyser.connect(scriptProcessorNode);\n    analyser.fftsize = bitSize;\n    scriptProcessorNode.connect(audioContext.destination);\n\n    // timing variables\n    const processInterval = 50; // how many ms between each computation\n    const intervalInFrames = (processInterval / 1000) * analyser.context.sampleRate;\n    let nextUpdateFrame = processInterval;\n\n    // process variables\n    let activityBuffer = 0;\n    let wasAboveThreshold = undefined;\n    let isAboveThreshold = false;\n\n    scriptProcessorNode.onaudioprocess = () => {\n        // throttles down the processing tic rate\n        nextUpdateFrame -= bitSize;\n        if (nextUpdateFrame >= 0) {\n            return;\n        }\n        nextUpdateFrame += intervalInFrames;\n\n        // computes volume and threshold\n        const normalizedVolume = getFrequencyAverage(\n            analyser,\n            frequencyRange[0],\n            frequencyRange[1]\n        );\n        if (normalizedVolume >= volumeThreshold) {\n            activityBuffer = minimumActiveCycles;\n        } else if (normalizedVolume < volumeThreshold && activityBuffer > 0) {\n            activityBuffer--;\n        }\n        isAboveThreshold = activityBuffer > 0;\n\n        onTic?.(normalizedVolume);\n        if (wasAboveThreshold !== isAboveThreshold) {\n            wasAboveThreshold = isAboveThreshold;\n            onThreshold?.(isAboveThreshold);\n        }\n    };\n    return {\n        disconnect: () => {\n            analyser.disconnect();\n            scriptProcessorNode.disconnect();\n            scriptProcessorNode.onaudioprocess = null;\n        },\n    };\n}\n\n/**\n * @param {MediaStreamSource} source\n * @param {AudioContext} audioContext\n * @param {Object} [param2] options\n * @returns {Object} returnValue\n * @returns {function} returnValue.disconnect disconnect callback\n */\nasync function _loadAudioWorkletProcessor(\n    source,\n    audioContext,\n    {\n        frequencyRange = HUMAN_VOICE_FREQUENCY_RANGE,\n        minimumActiveCycles = 10,\n        onThreshold,\n        onTic,\n        volumeThreshold = 0.3,\n    } = {}\n) {\n    await audioContext.resume();\n    // Safari does not support Worklet.addModule\n    await audioContext.audioWorklet.addModule(\"/mail/rtc/audio_worklet_processor\");\n    const thresholdProcessor = new window.AudioWorkletNode(audioContext, \"audio-processor\", {\n        processorOptions: {\n            minimumActiveCycles,\n            volumeThreshold,\n            frequencyRange,\n            postAllTics: !!onTic,\n        },\n    });\n    source.connect(thresholdProcessor);\n    thresholdProcessor.port.onmessage = (event) => {\n        const { isAboveThreshold, volume } = event.data;\n        if (isAboveThreshold !== undefined) {\n            onThreshold?.(isAboveThreshold);\n        }\n        if (volume !== undefined) {\n            onTic?.(volume);\n        }\n    };\n    return {\n        disconnect: () => {\n            thresholdProcessor.disconnect();\n        },\n    };\n}\n\n/**\n * @param {AnalyserNode} analyser\n * @param {number} lowerFrequency lower bound for relevant frequencies to monitor\n * @param {number} higherFrequency upper bound for relevant frequencies to monitor\n * @returns {number} normalized [0...1] average quantity of the relevant frequencies\n */\nfunction getFrequencyAverage(analyser, lowerFrequency, higherFrequency) {\n    const frequencies = new window.Uint8Array(analyser.frequencyBinCount);\n    analyser.getByteFrequencyData(frequencies);\n    const sampleRate = analyser.context.sampleRate;\n    const startIndex = _getFrequencyIndex(lowerFrequency, sampleRate, analyser.frequencyBinCount);\n    const endIndex = _getFrequencyIndex(higherFrequency, sampleRate, analyser.frequencyBinCount);\n    const count = endIndex - startIndex;\n    let sum = 0;\n    for (let index = startIndex; index < endIndex; index++) {\n        sum += frequencies[index] / 255;\n    }\n    if (!count) {\n        return 0;\n    }\n    return sum / count;\n}\n\n/**\n * @param {number} targetFrequency in Hz\n * @param {number} sampleRate the sample rate of the audio\n * @param {number} binCount AnalyserNode.frequencyBinCount\n * @returns {number} the index of the targetFrequency within binCount\n */\nfunction _getFrequencyIndex(targetFrequency, sampleRate, binCount) {\n    const index = Math.round((targetFrequency / (sampleRate / 2)) * binCount);\n    return Math.min(Math.max(0, index), binCount);\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport const STREAM_TYPE = Object.freeze({\n    AUDIO: \"audio\",\n    CAMERA: \"camera\",\n    SCREEN: \"screen\",\n});\nexport const UPDATE_EVENT = Object.freeze({\n    BROADCAST: \"broadcast\",\n    CONNECTION_CHANGE: \"connection_change\",\n    DISCONNECT: \"disconnect\",\n    INFO_CHANGE: \"info_change\",\n    TRACK: \"track\",\n});\nconst LOG_LEVEL = Object.freeze({\n    NONE: \"none\",\n    DEBUG: \"debug\",\n    INFO: \"info\",\n    WARN: \"warn\",\n    ERROR: \"error\",\n});\nconst INTERNAL_EVENT = Object.freeze({\n    ANSWER: \"answer\",\n    BROADCAST: \"broadcast\",\n    DISCONNECT: \"disconnect\",\n    ICE_CANDIDATE: \"ice-candidate\",\n    INFO: \"info\",\n    OFFER: \"offer\",\n    TRACK_CHANGE: \"trackChange\",\n});\nconst ORDERED_TRANSCEIVER_TYPES = [STREAM_TYPE.AUDIO, STREAM_TYPE.CAMERA, STREAM_TYPE.SCREEN];\nconst DEFAULT_BUS_BATCH_DELAY = 100;\nconst INITIAL_RECONNECT_DELAY = 2_000 + Math.random() * 1_000; // the initial delay between reconnection attempts\nconst MAXIMUM_RECONNECT_DELAY = 25_000 + Math.random() * 5_000; // the longest delay possible between reconnection attempts\nconst INVALID_ICE_CONNECTION_STATES = new Set([\"disconnected\", \"failed\", \"closed\"]);\nconst IS_CLIENT_RTC_COMPATIBLE = Boolean(window.RTCPeerConnection && window.MediaStream);\nconst DEFAULT_ICE_SERVERS = [\n    { urls: [\"stun:stun1.l.google.com:19302\", \"stun:stun2.l.google.com:19302\"] },\n];\nconst DEFAULT_NOTIFICATION_ROUTE = \"/mail/rtc/session/notify_call_members\";\n\n/**\n * @typedef {Object} Media\n * @property {MediaStreamTrack | null} track the track of the associated RtcRtpTransceiver, its presence does not\n *     imply active streaming as it exists for the whole lifetime transceiver (since webRTC 'unified plan').\n * @property {boolean} active represents whether the remote (peer) is actively streaming this track\n * @property {boolean} accepted represents whether the local (current user) wants to download this track\n */\n\n/**\n * @typedef {Object} Info (sealed)\n * @property {boolean} isSelfMuted\n * @property {boolean} isRaisingHand\n * @property {boolean} isDeaf\n * @property {boolean} isTalking\n * @property {boolean} isCameraOn\n * @property {boolean} isScreenSharingOn\n */\n\nexport class Peer {\n    /** @type {number} */\n    id;\n    /** @type {RTCPeerConnection} */\n    connection;\n    /** @type {number} */\n    connectRetryDelay = INITIAL_RECONNECT_DELAY;\n    /** @type {RTCDataChannel} */\n    dataChannel;\n    hasPriority = false;\n    isBuildingOffer = false;\n    isBuildingAnswer = false;\n    /** @type {Object<STREAM_TYPE[keyof STREAM_TYPE], Media>} */\n    medias = Object.seal({\n        [STREAM_TYPE.AUDIO]: {\n            track: null,\n            active: false,\n            accepted: true,\n        },\n        [STREAM_TYPE.SCREEN]: {\n            track: null,\n            active: false,\n            accepted: true,\n        },\n        [STREAM_TYPE.CAMERA]: {\n            track: null,\n            active: false,\n            accepted: true,\n        },\n    });\n    /**\n     * @param {number} id\n     * @param {Object} param2\n     * @param {RTCPeerConnection} param2.connection\n     * @param {RTCDataChannel} param2.dataChannel\n     * @param {boolean} hasPriority true if this peer offers should have priority in case of collisions\n     * @param {number} [connectRetryDelay=INITIAL_RECONNECT_DELAY]\n     */\n    constructor(\n        id,\n        {\n            connection,\n            dataChannel,\n            hasPriority = false,\n            connectRetryDelay = INITIAL_RECONNECT_DELAY,\n        }\n    ) {\n        this.id = id;\n        this.connection = connection;\n        this.dataChannel = dataChannel;\n        this.hasPriority = hasPriority;\n        this.connectRetryDelay = connectRetryDelay;\n        this.ready = new Promise((resolve) => {\n            this.dataChannel.addEventListener(\"open\", resolve);\n        });\n    }\n    disconnect() {\n        if (this.connection) {\n            const RTCRtpSenders = this.connection.getSenders();\n            for (const sender of RTCRtpSenders) {\n                try {\n                    this.connection.removeTrack(sender);\n                } catch {\n                    // ignore error\n                }\n            }\n            for (const transceiver of this.connection.getTransceivers()) {\n                try {\n                    transceiver.stop();\n                } catch {\n                    // transceiver may already be stopped by the remote.\n                }\n            }\n        }\n        this.connection?.close();\n        this.connection = undefined;\n        this.dataChannel?.close();\n        this.dataChannel = undefined;\n        for (const media of Object.values(this.medias)) {\n            media.track?.stop();\n        }\n    }\n    /**\n     * @param {{STREAM_TYPE[keyof STREAM_TYPE]}} streamType\n     * @param {boolean} canUpload whether this transceiver needs upload capability (outbound stream)\n     * @returns {RTCRtpTransceiverDirection}\n     */\n    getRecommendedTransceiverDirection(streamType, canUpload = false) {\n        if (this.medias[streamType].accepted) {\n            return canUpload ? \"sendrecv\" : \"recvonly\";\n        } else {\n            return canUpload ? \"sendonly\" : \"inactive\";\n        }\n    }\n    /**\n     * @param {STREAM_TYPE[keyof STREAM_TYPE]} streamType\n     * @returns {RTCRtpTransceiver | undefined} the transceiver used for this trackKind.\n     */\n    getTransceiver(streamType) {\n        if (!this.connection) {\n            // may be disconnected\n            return;\n        }\n        const transceivers = this.connection.getTransceivers();\n        return transceivers[ORDERED_TRANSCEIVER_TYPES.indexOf(streamType)];\n    }\n    /**\n     * @param {RTCRtpTransceiver} transceiver\n     * @returns {STREAM_TYPE[keyof STREAM_TYPE]}\n     */\n    getTransceiverStreamType(transceiver) {\n        const transceivers = this.connection.getTransceivers();\n        return ORDERED_TRANSCEIVER_TYPES[transceivers.indexOf(transceiver)];\n    }\n}\n\n/**\n * This class represents a network of peers and handles peer to peer connections.\n *\n *  @fires PeerToPeer#update\n */\nexport class PeerToPeer extends EventTarget {\n    /** @type {number} */\n    selfId;\n    /** @type {number}*/\n    channelId;\n    /** @type {Map<number, Peer>}*/\n    peers = new Map();\n    /** @type {number} */\n    _batchDelay = DEFAULT_BUS_BATCH_DELAY;\n    /** @type {Info} */\n    _localInfo = Object.seal({\n        isSelfMuted: false,\n        isRaisingHand: false,\n        isDeaf: false,\n        isTalking: false,\n        isCameraOn: false,\n        isScreenSharingOn: false,\n    });\n    /** @type {String[]} */\n    _iceServers;\n    _isPendingNotify = false;\n    _notificationsToSend = new Map();\n    _isAntiGlareEnabled = true;\n    /**\n     * id of notification transaction\n     * @type {number}\n     */\n    _tmpNotificationId = 0;\n    /**\n     * by peer ID\n     * @type {Map<timeoutID>}\n     */\n    _recoverTimeouts = new Map();\n    /** @type {String} */\n    _notificationRoute;\n    /** @type {boolean} */\n    _isStreamingEnabled = true;\n    /** @type {Object<STREAM_TYPE[keyof STREAM_TYPE], MediaStreamTrack | null>} */\n    _tracks = Object.seal({\n        [STREAM_TYPE.AUDIO]: null,\n        [STREAM_TYPE.SCREEN]: null,\n        [STREAM_TYPE.CAMERA]: null,\n    });\n    _loggingFunctions = {\n        [LOG_LEVEL.DEBUG]: () => {},\n        [LOG_LEVEL.INFO]: () => {},\n        [LOG_LEVEL.WARN]: () => {},\n        [LOG_LEVEL.ERROR]: () => {},\n    };\n    /**\n     * @param {object} [options]\n     * @param {String} [options.notificationRoute] the route used to communicate with the odoo server\n     * @param {LOG_LEVEL[keyof LOG_LEVEL]} [options.logLevel=LOG_LEVEL.NONE]\n     * @param {boolean} [options.antiGlare=true] whether or not to use the rollback feature to manage offer collisions,\n     *        ids provided for peers should be comparable for this feature to work.\n     * @param {number} [options.batchDelay=DEFAULT_BUS_BATCH_DELAY]\n     * @param {boolean} [options.enableStreaming=true] whether or not setting the peer connections with audio and video\n     *        transceivers to allow streaming features.\n     */\n    constructor({\n        notificationRoute = DEFAULT_NOTIFICATION_ROUTE,\n        logLevel = LOG_LEVEL.NONE,\n        batchDelay = DEFAULT_BUS_BATCH_DELAY,\n        antiGlare = true,\n        enableStreaming = true,\n    } = {}) {\n        super();\n        this._isStreamingEnabled = enableStreaming;\n        this._isAntiGlareEnabled = antiGlare;\n        this._notificationRoute = notificationRoute;\n        this._batchDelay = batchDelay;\n        this.setLoggingLevel(logLevel);\n    }\n\n    /**\n     * @param {any} selfId should be comparable to benefit from the anti glare (offer collisions)\n     * @param {any} channelId\n     * @param {object} [options]\n     * @param {Info} [options.info={}]\n     * @param {array} [options.iceServers=DEFAULT_ICE_SERVERS]\n     */\n    connect(selfId, channelId, { info = {}, iceServers = DEFAULT_ICE_SERVERS } = {}) {\n        if (!IS_CLIENT_RTC_COMPATIBLE) {\n            throw new Error(\"RTCPeerConnection is not supported\");\n        }\n        this.selfId = selfId;\n        this.channelId = channelId;\n        this._iceServers = iceServers;\n        this._localInfo = Object.assign(this._localInfo, info);\n    }\n\n    removeALlPeers() {\n        for (const peer of this.peers.values()) {\n            this.removePeer(peer.id);\n        }\n        this.peers.clear();\n    }\n\n    disconnect() {\n        this.removeALlPeers();\n        this.selfId = undefined;\n        this.channelId = undefined;\n        this._localInfo = Object.assign(this._localInfo, {\n            isSelfMuted: false,\n            isRaisingHand: false,\n            isDeaf: false,\n            isTalking: false,\n            isCameraOn: false,\n            isScreenSharingOn: false,\n        });\n    }\n    /**\n     * Adds a peer and starts the process of connection establishment. From this point the whole\n     * peer lifecycle is handled internally, including connection recovery attempts, until\n     * `removePeer()` or `disconnect()` is called.\n     * If a peer of that id already exists, it is returned without being re-created.\n     * This allows `addPeer` to be called to ensure that all of them are registered without fear\n     * of resetting connections (removePeer() should be called explicitly if that is the intention).\n     *\n     * @param {number} id\n     * @param {object} [options={}] options for the Peer constructor\n     * @returns {Peer} resolved when the dataChannel is open\n     */\n    async addPeer(id, options = {}) {\n        const peer = this.peers.get(id);\n        if (peer) {\n            return peer;\n        }\n        const newPeer = this._createPeer(id, options);\n        await newPeer.ready;\n        return newPeer;\n    }\n    removePeer(id) {\n        const recoverTimeoutId = this._recoverTimeouts.get(id);\n        browser.clearTimeout(recoverTimeoutId);\n        this._recoverTimeouts.delete(id);\n        const peer = this.peers.get(id);\n        if (!peer) {\n            return;\n        }\n        this.peers.delete(id);\n        peer.disconnect();\n    }\n\n    /**\n     * Broadcast a message to all peers\n     * @param message any JSON serializable\n     */\n    broadcast(message) {\n        this._dataChannelBroadcast(INTERNAL_EVENT.BROADCAST, message);\n    }\n    /**\n     * @param id\n     * @return {{\n     *     connectionState: RTCPeerConnection.connectionState\n     *     iceConnectionState: RTCPeerConnection.iceConnectionState\n     *     iceGatheringState: RTCPeerConnection.iceGatheringState\n     *     localCandidateType: RTCIceCandidatePairStats.candidateType\n     *     remoteCandidateType: RTCIceCandidatePairStats.candidateType\n     *     dataChannelState:  RTCDataChannelStats.state\n     *     dtlsState: RTCTransportStats.dtpsState,\n     *     iceState: RTCTransportStats.iceState,\n     *     packetsReceived: RTCTransportStats.packetsReceived,\n     *     packetsSent: RTCTransportStats.packetsSent,\n     * } | {}}\n     */\n    async getFormattedStats(id) {\n        const peer = this.peers.get(id);\n        const formattedStats = {};\n        if (!peer) {\n            return formattedStats;\n        }\n        formattedStats.connectionState = peer.connection.connectionState;\n        formattedStats.iceConnectionState = peer.connection.iceConnectionState;\n        formattedStats.iceGatheringState = peer.connection.iceGatheringState;\n        const stats = await peer.connection.getStats();\n        for (const value of stats?.values() || []) {\n            switch (value.type) {\n                case \"candidate-pair\":\n                    if (value.state === \"succeeded\" && value.localCandidateId) {\n                        formattedStats.localCandidateType =\n                            stats.get(value.localCandidateId)?.candidateType || \"\";\n                        formattedStats.remoteCandidateType =\n                            stats.get(value.remoteCandidateId)?.candidateType || \"\";\n                    }\n                    break;\n                case \"data-channel\":\n                    formattedStats.dataChannelState = value.state;\n                    break;\n                case \"transport\":\n                    formattedStats.dtlsState = value.dtlsState;\n                    formattedStats.iceState = value.iceState;\n                    formattedStats.packetsReceived = value.packetsReceived;\n                    formattedStats.packetsSent = value.packetsSent;\n                    break;\n            }\n        }\n        return formattedStats;\n    }\n    /**\n     * Stop or resume the consumption of tracks from the other call participants.\n     *\n     * @param {number} id\n     * @param {Object<[STREAM_TYPE[keyof STREAM_TYPE], boolean]>} states e.g: { screen: true, camera: false }\n     */\n    updateDownload(id, states) {\n        const peer = this.peers.get(id);\n        if (!peer) {\n            return;\n        }\n        for (const [streamType, accepted] of Object.entries(states)) {\n            peer.medias[streamType].accepted = accepted;\n            const transceiver = peer.getTransceiver(streamType);\n            if (!transceiver) {\n                this._recover(id, `no transceiver available when updating direction`);\n                return;\n            }\n            // changing the direction triggers a negotiation-needed\n            transceiver.direction = peer.getRecommendedTransceiverDirection(\n                streamType,\n                Boolean(this._tracks[streamType])\n            );\n        }\n    }\n\n    /**\n     * @param {STREAM_TYPE[keyof STREAM_TYPE]} streamType\n     * @param {MediaStreamTrack | null} [track] track to be sent to the other call participants\n     */\n    async updateUpload(streamType, track) {\n        this._tracks[streamType] = track || null;\n        this.updateInfo({\n            isScreenSharingOn: Boolean(this._tracks[STREAM_TYPE.SCREEN]),\n            isCameraOn: Boolean(this._tracks[STREAM_TYPE.CAMERA]),\n        });\n        const proms = [];\n        for (const peer of this.peers.values()) {\n            proms.push(this._updateRemote(peer, streamType));\n        }\n        await Promise.all(proms);\n    }\n    /**\n     * @param {Info} info\n     */\n    updateInfo(info) {\n        this._localInfo = Object.assign(this._localInfo, info);\n        this._dataChannelBroadcast(INTERNAL_EVENT.INFO, this._localInfo);\n    }\n    /**\n     * @param id id of the peer sending the notification\n     * @param {string} content JSON\n     */\n    async handleNotification(id, content) {\n        /** @type {{ event: INTERNAL_EVENT[keyof INTERNAL_EVENT], channelId, payload: Object }} */\n        const { event, channelId, payload } = JSON.parse(content);\n        this._emitLog(id, `received notification: ${event}`, LOG_LEVEL.DEBUG);\n        if (channelId !== this.channelId) {\n            return;\n        }\n        let peer = this.peers.get(id);\n        if (event !== INTERNAL_EVENT.OFFER && !peer?.connection) {\n            this._emitLog(id, `received ${event} for missing peer ${id}`, LOG_LEVEL.WARN);\n            return;\n        }\n        switch (event) {\n            case INTERNAL_EVENT.ANSWER: {\n                this._emitLog(id, `received answer`, LOG_LEVEL.DEBUG);\n                if (\n                    INVALID_ICE_CONNECTION_STATES.has(peer.connection.iceConnectionState) ||\n                    peer.connection.signalingState === \"stable\" ||\n                    peer.connection.signalingState === \"have-remote-offer\"\n                ) {\n                    return;\n                }\n                const description = new window.RTCSessionDescription(payload.sdp);\n                try {\n                    await peer.connection.setRemoteDescription(description);\n                } catch {\n                    this._recover(id, \"answer handling: Failed at setting remoteDescription\");\n                    // ignored the transaction may have been resolved by another concurrent offer.\n                }\n                break;\n            }\n            case INTERNAL_EVENT.BROADCAST: {\n                this._emitUpdate({\n                    name: UPDATE_EVENT.BROADCAST,\n                    payload: { senderId: id, message: payload },\n                });\n                break;\n            }\n            case INTERNAL_EVENT.DISCONNECT: {\n                this.removePeer(id);\n                this._emitUpdate({ name: UPDATE_EVENT.DISCONNECT, payload: { sessionId: id } });\n                break;\n            }\n            case INTERNAL_EVENT.ICE_CANDIDATE: {\n                if (INVALID_ICE_CONNECTION_STATES.has(peer.connection.iceConnectionState)) {\n                    return;\n                }\n                const rtcIceCandidate = new window.RTCIceCandidate(payload.candidate);\n                try {\n                    await peer.connection.addIceCandidate(rtcIceCandidate);\n                } catch {\n                    this._recover(id, \"failed at adding ice candidate\");\n                }\n                break;\n            }\n            case INTERNAL_EVENT.INFO: {\n                const { isTalking, isCameraOn, isScreenSharingOn } = payload;\n                peer.medias[STREAM_TYPE.AUDIO].active = isTalking;\n                peer.medias[STREAM_TYPE.CAMERA].active = isCameraOn;\n                peer.medias[STREAM_TYPE.SCREEN].active = isScreenSharingOn;\n                this._emitUpdate({\n                    name: UPDATE_EVENT.INFO_CHANGE,\n                    payload: { [id]: payload },\n                });\n                break;\n            }\n            case INTERNAL_EVENT.OFFER: {\n                if (!peer) {\n                    peer = this._createPeer(id);\n                }\n                if (\n                    INVALID_ICE_CONNECTION_STATES.has(peer.connection.iceConnectionState) ||\n                    peer.connection.signalingState === \"have-remote-offer\"\n                ) {\n                    return;\n                }\n                const isStable =\n                    peer.connection.signalingState === \"stable\" || peer.isBuildingAnswer;\n                const hasOfferCollision = !isStable || peer.isBuildingOffer;\n                if (hasOfferCollision && peer.hasPriority && this._isAntiGlareEnabled) {\n                    this._emitLog(\n                        peer.id,\n                        `rolling back due to offer collision: ${peer.connection.signalingState}`,\n                        LOG_LEVEL.WARN\n                    );\n                    try {\n                        await peer.connection.setLocalDescription({ type: \"rollback\" });\n                    } catch {\n                        this._recover(id, `failed rollback`);\n                    }\n                }\n                const description = new window.RTCSessionDescription(payload.sdp);\n                try {\n                    await peer.connection.setRemoteDescription(description);\n                } catch {\n                    this._recover(id, \"failed at setting remoteDescription\");\n                    return;\n                }\n                if (this._isStreamingEnabled) {\n                    if (peer.connection.getTransceivers().length === 0) {\n                        for (const streamType of ORDERED_TRANSCEIVER_TYPES) {\n                            const type = streamType === STREAM_TYPE.AUDIO ? \"audio\" : \"video\";\n                            peer.connection.addTransceiver(type);\n                        }\n                    }\n                    for (const transceiverName of ORDERED_TRANSCEIVER_TYPES) {\n                        await this._updateRemote(peer, transceiverName);\n                    }\n                }\n                peer.isBuildingAnswer = true;\n                try {\n                    await peer.connection.setLocalDescription(await peer.connection.createAnswer());\n                } catch {\n                    peer.isBuildingAnswer = false;\n                    this._recover(id, \"offer handling: failed at setting answer localDescription\");\n                    return;\n                }\n                peer.isBuildingAnswer = false;\n                this._emitLog(id, `sending answer`, LOG_LEVEL.DEBUG);\n                await this._busNotify(INTERNAL_EVENT.ANSWER, {\n                    payload: {\n                        sdp: peer.connection.localDescription,\n                    },\n                    targets: [peer.id],\n                });\n                this._recover(peer.id, \"standard answer timeout\");\n                break;\n            }\n        }\n    }\n    /**\n     * @param {LOG_LEVEL[keyof LOG_LEVEL]} logLevel\n     */\n    setLoggingLevel(logLevel) {\n        const makeLog = (level) => {\n            return (id, message) => {\n                this.dispatchEvent(new CustomEvent(\"log\", { detail: { id, level, message } }));\n            };\n        };\n        this._loggingFunctions = {\n            [LOG_LEVEL.DEBUG]: () => {},\n            [LOG_LEVEL.INFO]: () => {},\n            [LOG_LEVEL.WARN]: () => {},\n            [LOG_LEVEL.ERROR]: () => {},\n        };\n        switch (logLevel) {\n            case LOG_LEVEL.DEBUG:\n                this._loggingFunctions[LOG_LEVEL.DEBUG] = makeLog(LOG_LEVEL.DEBUG);\n            // eslint-disable-next-line no-fallthrough\n            case LOG_LEVEL.INFO:\n                this._loggingFunctions[LOG_LEVEL.INFO] = makeLog(LOG_LEVEL.INFO);\n            // eslint-disable-next-line no-fallthrough\n            case LOG_LEVEL.WARN:\n                this._loggingFunctions[LOG_LEVEL.WARN] = makeLog(LOG_LEVEL.WARN);\n            // eslint-disable-next-line no-fallthrough\n            case LOG_LEVEL.ERROR:\n                this._loggingFunctions[LOG_LEVEL.ERROR] = makeLog(LOG_LEVEL.ERROR);\n        }\n    }\n    /**\n     * @param {INTERNAL_EVENT[keyof INTERNAL_EVENT]} internalEvent\n     * @param message any JSON serializable\n     */\n    _dataChannelBroadcast(internalEvent, message) {\n        for (const peer of this.peers.values()) {\n            if (!peer?.dataChannel || peer?.dataChannel.readyState !== \"open\") {\n                continue;\n            }\n            peer.dataChannel.send(\n                JSON.stringify({\n                    event: internalEvent,\n                    channelId: this.channelId,\n                    payload: message,\n                })\n            );\n        }\n    }\n    /**\n     * @param {any} detail\n     */\n    _emitUpdate(detail) {\n        this.dispatchEvent(new CustomEvent(\"update\", { detail }));\n    }\n    /**\n     * @param id\n     * @param {string} message\n     * @param {LOG_LEVEL[keyof LOG_LEVEL]} [level=LOG_LEVEL.DEBUG]\n     */\n    _emitLog(id, message, level = LOG_LEVEL.DEBUG) {\n        this._loggingFunctions[level](id, message);\n    }\n    /**\n     * @param id\n     * @param {string} reason\n     */\n    _recover(id, reason = \"\") {\n        if (this._recoverTimeouts.get(id)) {\n            return;\n        }\n        const peer = this.peers.get(id);\n        if (!peer) {\n            return;\n        }\n        // Retry connecting with an exponential backoff.\n        const delay =\n            Math.min(peer.connectRetryDelay * 1.5, MAXIMUM_RECONNECT_DELAY) + 1000 * Math.random();\n        this._recoverTimeouts.set(\n            id,\n            browser.setTimeout(async () => {\n                const peer = this.peers.get(id);\n                this._recoverTimeouts.delete(id);\n                if (\n                    !peer?.connection ||\n                    !this.channelId ||\n                    peer.connection.iceConnectionState === \"connected\" ||\n                    peer.connection.iceConnectionState === \"completed\"\n                ) {\n                    return;\n                }\n                this._emitLog(id, `attempting to recover connection: ${reason}`, LOG_LEVEL.WARN);\n                this._busNotify(INTERNAL_EVENT.DISCONNECT, { targets: [peer.id] });\n                this.removePeer(peer.id);\n                this.addPeer(peer.id, { connectRetryDelay: delay });\n            }, delay)\n        );\n    }\n    async _sendNotifications() {\n        if (this._isPendingNotify) {\n            return;\n        }\n        this._isPendingNotify = true;\n        await new Promise((resolve) => setTimeout(resolve, this._batchDelay));\n        const ids = [];\n        const notifications = [];\n        this._notificationsToSend.forEach((notification, id) => {\n            ids.push(id);\n            notifications.push([\n                notification.sender,\n                notification.targets,\n                JSON.stringify({\n                    event: notification.event,\n                    channelId: notification.channelId,\n                    payload: notification.payload,\n                }),\n            ]);\n        });\n        try {\n            await rpc(\n                this._notificationRoute,\n                {\n                    peer_notifications: notifications,\n                },\n                { silent: true }\n            );\n            for (const id of ids) {\n                this._notificationsToSend.delete(id);\n            }\n        } finally {\n            this._isPendingNotify = false;\n            if (this._notificationsToSend.size > 0) {\n                await this._sendNotifications();\n            }\n        }\n    }\n    /**\n     * @param {INTERNAL_EVENT[keyof INTERNAL_EVENT]} event\n     * @param {Object} [options]\n     * @param {Object} [options.payload]\n     * @param {number[]} [options.targets] list of the ids of peers to send the message to,\n     * sends to all peers if no specified target(s)\n     */\n    async _busNotify(event, { payload, targets } = {}) {\n        targets = targets || Array.from(this.peers.keys());\n        let id;\n        if (event === INTERNAL_EVENT.OFFER) {\n            // offers are always single-target, ensures that only 1 offer (the latest) per target is kept\n            id = `latestOffer_to:${targets[0]}`;\n        } else {\n            id = ++this._tmpNotificationId;\n        }\n        this._notificationsToSend.set(id, {\n            channelId: this.channelId,\n            event,\n            payload,\n            sender: this.selfId,\n            targets,\n        });\n        await this._sendNotifications();\n    }\n    /**\n     * @param {Peer} peer\n     * @param {STREAM_TYPE[keyof STREAM_TYPE]} streamType\n     */\n    async _updateRemote(peer, streamType) {\n        const track = this._tracks[streamType];\n        const transceiver = peer.getTransceiver(streamType);\n        if (!transceiver) {\n            return;\n        }\n        try {\n            await transceiver.sender.replaceTrack(track);\n            transceiver.direction = peer.getRecommendedTransceiverDirection(\n                streamType,\n                Boolean(track)\n            );\n        } catch (error) {\n            this._recover(\n                peer.id,\n                `failed to update ${streamType} transceiver for peer ${peer.id}: ${error}`\n            );\n        }\n    }\n    /**\n     * Creates a new peer.\n     * If a peer of this id already exists, it is cleared.\n     *\n     * @param {number} id\n     * @param {object} [options={}]\n     * @returns {Peer}\n     */\n    _createPeer(id, options = {}) {\n        this.removePeer(id);\n        const peerConnection = new window.RTCPeerConnection({ iceServers: this._iceServers });\n        const dataChannel = peerConnection.createDataChannel(\"notifications\", {\n            negotiated: true,\n            id: 1,\n        });\n        const peer = new Peer(id, {\n            ...options,\n            connection: peerConnection,\n            dataChannel,\n            hasPriority: id > this.selfId,\n        });\n        this.peers.set(id, peer);\n        peerConnection.addEventListener(\"icecandidate\", async (event) => {\n            if (!event.candidate) {\n                return;\n            }\n            await this._busNotify(INTERNAL_EVENT.ICE_CANDIDATE, {\n                payload: {\n                    candidate: event.candidate,\n                },\n                targets: [id],\n            });\n        });\n        peerConnection.addEventListener(\"iceconnectionstatechange\", async () => {\n            this._emitUpdate({\n                name: UPDATE_EVENT.CONNECTION_CHANGE,\n                payload: { id, peer, state: peerConnection.iceConnectionState },\n            });\n            switch (peerConnection.iceConnectionState) {\n                case \"closed\":\n                    this.removePeer(id);\n                    break;\n                case \"failed\":\n                case \"disconnected\":\n                    this._recover(peer.id, 1000, \"ice connection disconnected\");\n                    break;\n            }\n        });\n        peerConnection.addEventListener(\"icegatheringstatechange\", () => {\n            this._emitLog(\n                id,\n                `gathering state change: ${peerConnection.iceGatheringState}`,\n                LOG_LEVEL.INFO\n            );\n        });\n        peerConnection.addEventListener(\"connectionstatechange\", async () => {\n            this._emitLog(\n                id,\n                `connection state change: ${peerConnection.connectionState}`,\n                LOG_LEVEL.INFO\n            );\n        });\n        peerConnection.addEventListener(\"icecandidateerror\", async (error) => {\n            this._recover(id, `ice candidate error: ${error.errorText}`);\n        });\n        peerConnection.addEventListener(\"negotiationneeded\", async () => {\n            peer.isBuildingOffer = true;\n            try {\n                await peerConnection.setLocalDescription(await peerConnection.createOffer());\n            } catch (error) {\n                this._recover(id, `failed to set local Description for offer: ${error}`);\n                peer.isBuildingOffer = false;\n                return;\n            }\n            peer.isBuildingOffer = false;\n            await this._busNotify(INTERNAL_EVENT.OFFER, {\n                payload: {\n                    sdp: peerConnection.localDescription,\n                },\n                targets: [id],\n            });\n        });\n        peerConnection.addEventListener(\"track\", ({ transceiver, track }) => {\n            const streamType = peer.getTransceiverStreamType(transceiver);\n            peer.medias[streamType].track = track;\n            this._emitUpdate({\n                name: UPDATE_EVENT.TRACK,\n                payload: {\n                    sessionId: id,\n                    type: streamType,\n                    track,\n                    active: peer.medias[streamType].active,\n                },\n            });\n        });\n        dataChannel.addEventListener(\"message\", async (event) => {\n            await this.handleNotification(id, event.data);\n        });\n        dataChannel.addEventListener(\"open\", () => {\n            if (dataChannel.readyState !== \"open\") {\n                // can be closed by the time the event is emitted\n                return;\n            }\n            dataChannel.send(\n                JSON.stringify({\n                    event: INTERNAL_EVENT.INFO,\n                    channelId: this.channelId,\n                    payload: this._localInfo,\n                })\n            );\n        });\n        return peer;\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class PttAdBanner extends Component {\n    static template = \"discuss.pttAdBanner\";\n    static props = {};\n    static LOCAL_STORAGE_KEY = \"ptt_ad_banner_discarded\";\n\n    setup() {\n        super.setup();\n        this.pttExtService = useState(useService(\"discuss.ptt_extension\"));\n        this.store = useState(useService(\"mail.store\"));\n        this.state = useState({\n            wasDiscarded: browser.localStorage.getItem(PttAdBanner.LOCAL_STORAGE_KEY),\n        });\n    }\n\n    onClickClose() {\n        browser.localStorage.setItem(PttAdBanner.LOCAL_STORAGE_KEY, true);\n        this.state.wasDiscarded = true;\n    }\n\n    get isVisible() {\n        return (\n            !this.pttExtService.isEnabled &&\n            this.store.settings.use_push_to_talk &&\n            !isMobileOS() &&\n            !this.state.wasDiscarded\n        );\n    }\n}\n", "import { markup } from \"@odoo/owl\";\n\nimport { parseVersion } from \"@mail/utils/common/misc\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { escape, sprintf } from \"@web/core/utils/strings\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport const pttExtensionHookService = {\n    start(env) {\n        const INITIAL_RELEASE_TIMEOUT = 500;\n        const COMMON_RELEASE_TIMEOUT = 200;\n        // https://chromewebstore.google.com/detail/discuss-push-to-talk/mdiacebcbkmjjlpclnbcgiepgifcnpmg\n        const EXT_ID = \"mdiacebcbkmjjlpclnbcgiepgifcnpmg\";\n        const versionPromise =\n            window.chrome?.runtime?.sendMessage(EXT_ID, { type: \"ask-version\" }) ??\n            Promise.resolve(\"1.0.0.0\");\n        let isEnabled = false;\n        let voiceActivated = false;\n\n        browser.addEventListener(\"message\", ({ data, origin, source }) => {\n            const rtc = env.services[\"discuss.rtc\"];\n            if (\n                source !== window ||\n                origin !== location.origin ||\n                data.from !== \"discuss-push-to-talk\" ||\n                (!rtc && data.type !== \"answer-is-enabled\")\n            ) {\n                return;\n            }\n            switch (data.type) {\n                case \"push-to-talk-pressed\":\n                    {\n                        voiceActivated = false;\n                        const isFirstPress = !rtc.selfSession?.isTalking;\n                        rtc.onPushToTalk();\n                        if (rtc.selfSession?.isTalking) {\n                            // Second key press is slow to come thus, the first timeout\n                            // must be greater than the following ones.\n                            rtc.setPttReleaseTimeout(\n                                isFirstPress ? INITIAL_RELEASE_TIMEOUT : COMMON_RELEASE_TIMEOUT\n                            );\n                        }\n                    }\n                    break;\n                case \"toggle-voice\":\n                    {\n                        if (voiceActivated) {\n                            rtc.setPttReleaseTimeout(0);\n                        } else {\n                            rtc.onPushToTalk();\n                        }\n                        voiceActivated = !voiceActivated;\n                    }\n                    break;\n                case \"answer-is-enabled\":\n                    isEnabled = true;\n                    break;\n            }\n        });\n\n        /**\n         * Send a message to the PTT extension.\n         *\n         * @param {\"ask-is-enabled\" | \"subscribe\" | \"unsubscribe\" | \"is-talking\"} type\n         * @param {*} value\n         */\n        async function sendMessage(type, value) {\n            if (!isEnabled && type !== \"ask-is-enabled\") {\n                return;\n            }\n            const version = parseVersion(await versionPromise);\n            if (version.isLowerThan(\"1.0.0.2\")) {\n                window.postMessage({ from: \"discuss\", type, value }, location.origin);\n                return;\n            }\n            window.chrome?.runtime?.sendMessage(EXT_ID, { type, value });\n        }\n\n        sendMessage(\"ask-is-enabled\");\n\n        return {\n            notifyIsTalking(isTalking) {\n                sendMessage(\"is-talking\", isTalking);\n            },\n            subscribe() {\n                sendMessage(\"subscribe\");\n            },\n            unsubscribe() {\n                voiceActivated = false;\n                sendMessage(\"unsubscribe\");\n            },\n            get isEnabled() {\n                return isEnabled;\n            },\n            downloadURL: `https://chromewebstore.google.com/detail/discuss-push-to-talk/${EXT_ID}`,\n            get downloadText() {\n                const translation = _t(\n                    `The Push-to-Talk feature is only accessible within tab focus. To enable the Push-to-Talk functionality outside of this tab, we recommend downloading our %(anchor_start)sextension%(anchor_end)s.`\n                );\n                return markup(\n                    sprintf(escape(translation), {\n                        anchor_start: `<a href=\"${this.downloadURL}\" target=\"_blank\" class=\"text-reset text-decoration-underline\">`,\n                        anchor_end: \"</a>\",\n                    })\n                );\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.ptt_extension\", pttExtensionHookService);\n", "import { Record } from \"@mail/core/common/record\";\nimport { BlurManager } from \"@mail/discuss/call/common/blur_manager\";\nimport { monitorAudio } from \"@mail/discuss/call/common/media_monitoring\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { closeStream, onChange } from \"@mail/utils/common/misc\";\n\nimport { reactive } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { callActionsRegistry } from \"./call_actions\";\n\n/**\n * @typedef {'audio' | 'camera' | 'screen' } streamType\n */\n\n/**\n * @return {Promise<{ SfuClient: import(\"@mail/../lib/odoo_sfu/odoo_sfu\").SfuClient, SFU_CLIENT_STATE: import(\"@mail/../lib/odoo_sfu/odoo_sfu\").SFU_CLIENT_STATE }>}\n */\nconst loadSfuAssets = memoize(async () => await loadBundle(\"mail.assets_odoo_sfu\"));\n\nexport const CONNECTION_TYPES = { P2P: \"p2p\", SERVER: \"server\" };\nconst SCREEN_CONFIG = {\n    width: { max: 1920 },\n    height: { max: 1080 },\n    aspectRatio: 16 / 9,\n    frameRate: {\n        max: 24,\n    },\n};\nconst CAMERA_CONFIG = {\n    width: { max: 1280 },\n    height: { max: 720 },\n    aspectRatio: 16 / 9,\n    frameRate: {\n        max: 30,\n    },\n};\nconst IS_CLIENT_RTC_COMPATIBLE = Boolean(window.RTCPeerConnection && window.MediaStream);\nconst DEFAULT_ICE_SERVERS = [\n    { urls: [\"stun:stun1.l.google.com:19302\", \"stun:stun2.l.google.com:19302\"] },\n];\n\n/**\n * @param {Array<RTCIceServer>} iceServers\n * @returns {Boolean}\n */\nfunction hasTurn(iceServers) {\n    return iceServers.some((server) => {\n        let hasTurn = false;\n        if (server.url) {\n            hasTurn = server.url.startsWith(\"turn:\");\n        }\n        if (server.urls) {\n            if (Array.isArray(server.urls)) {\n                hasTurn = server.urls.some((url) => url.startsWith(\"turn:\")) || hasTurn;\n            } else {\n                hasTurn = server.urls.startsWith(\"turn:\") || hasTurn;\n            }\n        }\n        return hasTurn;\n    });\n}\n\n/**\n * Allows to use both peer to peer and SFU connections simultaneously, which makes it possible to\n * establish a connection with other call participants with the SFU when possible, and still handle\n * peer-to-peer for the participants who did not manage to establish a SFU connection.\n */\nclass Network {\n    /** @type {import(\"@mail/discuss/call/common/peer_to_peer\").PeerToPeer} */\n    p2p;\n    /** @type {import(\"@mail/../lib/odoo_sfu/odoo_sfu\").SfuClient} */\n    sfu;\n    /** @type {array[{ name: string, f: EventListener }]} */\n    _listeners = [];\n    /**\n     * @param {import(\"@mail/discuss/call/common/peer_to_peer\").PeerToPeer} p2p\n     * @param {import(\"@mail/../lib/odoo_sfu/odoo_sfu\").SfuClient} [sfu]\n     */\n    constructor(p2p, sfu) {\n        this.p2p = p2p;\n        this.sfu = sfu;\n    }\n\n    /**\n     * add a SFU to the network.\n     * @param {import(\"@mail/../lib/odoo_sfu/odoo_sfu\").SfuClient} sfu\n     */\n    addSfu(sfu) {\n        if (this.sfu) {\n            this.sfu.disconnect();\n        }\n        this.sfu = sfu;\n    }\n    /**\n     * @param {string} name\n     * @param {function} f\n     * @override\n     */\n    addEventListener(name, f) {\n        this._listeners.push({ name, f });\n        this.p2p.addEventListener(name, f);\n        this.sfu?.addEventListener(name, f);\n    }\n    /**\n     * @param {streamType} type\n     * @param {MediaStreamTrack | null} track track to be sent to the other call participants,\n     * not setting it will remove the track from the server\n     */\n    async updateUpload(type, track) {\n        await this.p2p.updateUpload(type, track);\n        await this.sfu?.updateUpload(type, track);\n    }\n    /**\n     * Stop or resume the consumption of tracks from the other call participants.\n     *\n     * @param {number} sessionId\n     * @param {Object<[streamType, boolean]>} states e.g: { audio: true, camera: false }\n     */\n    updateDownload(sessionId, states) {\n        this.p2p.updateDownload(sessionId, states);\n        this.sfu?.updateDownload(sessionId, states);\n    }\n    /**\n     * Updates the server with the info of the session (isTalking, isCameraOn,...) so that it can broadcast it to the\n     * other call participants.\n     *\n     * @param {import(\"#src/models/session.js\").SessionInfo} info\n     * @param {Object} [options] see documentation of respective classes\n     */\n    updateInfo(info, options = {}) {\n        this.p2p.updateInfo(info, options);\n        this.sfu?.updateInfo(info, options);\n    }\n    disconnect() {\n        for (const { name, f } of this._listeners.splice(0)) {\n            this.p2p.removeEventListener(name, f);\n            this.sfu?.removeEventListener(name, f);\n        }\n        this.p2p.disconnect();\n        this.sfu?.disconnect();\n    }\n}\n\nexport class Rtc extends Record {\n    /** @returns {import(\"models\").Rtc} */\n    static get(data) {\n        return super.get(...arguments);\n    }\n    /** @returns {import(\"models\").Rtc} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    notifications = reactive(new Map());\n    /** @type {Map<string, number>} timeoutId by notificationId for call notifications */\n    timeouts = new Map();\n    /** @type {Map<number, number>} timeoutId by sessionId for download pausing delay */\n    downloadTimeouts = new Map();\n    iceServers = Record.attr(DEFAULT_ICE_SERVERS, {\n        compute() {\n            return this.iceServers ? this.iceServers : DEFAULT_ICE_SERVERS;\n        },\n    });\n    selfSession = Record.one(\"RtcSession\");\n    serverInfo;\n    /**\n     * @type {Network}\n     */\n    network;\n    /** @type {import(\"@mail/../lib/odoo_sfu/odoo_sfu\").SfuClient} */\n    sfuClient = undefined;\n\n    /** @type {Object<string, boolean>} The keys are action names and the values are booleans indicating whether each action is active */\n    lastActions = {};\n    /** @type {Array<string>} Array of action names representing the stack of currently active actions */\n    actionsStack = [];\n    /** @type {string|undefined} String representing the last call action activated, or undefined if none are */\n    lastSelfCallAction = undefined;\n\n    callActions = Record.attr([], {\n        compute() {\n            return callActionsRegistry\n                .getEntries()\n                .filter(([key, action]) => action.condition({ rtc: this }))\n                .map(([key, action]) => [key, action.isActive({ rtc: this })]);\n        },\n        onUpdate() {\n            for (const [key, isActive] of this.callActions) {\n                if (isActive === this.lastActions[key]) {\n                    continue;\n                }\n                if (isActive) {\n                    if (!this.actionsStack.includes(key)) {\n                        this.actionsStack.unshift(key);\n                    }\n                } else {\n                    this.actionsStack.splice(this.actionsStack.indexOf(key), 1);\n                }\n            }\n\n            this.lastSelfCallAction = this.actionsStack[0];\n\n            this.lastActions = Object.fromEntries(this.callActions);\n        },\n    });\n\n    setup() {\n        this.linkVoiceActivationDebounce = debounce(this.linkVoiceActivation, 500);\n        this.state = reactive({\n            connectionType: undefined,\n            hasPendingRequest: false,\n            channel: undefined,\n            logs: new Map(),\n            sendCamera: false,\n            sendScreen: false,\n            serverState: undefined,\n            updateAndBroadcastDebounce: undefined,\n            audioTrack: undefined,\n            cameraTrack: undefined,\n            screenTrack: undefined,\n            /**\n             * callback to properly end the audio monitoring.\n             * If set it indicates that we are currently monitoring the local\n             * audioTrack for the voice activation feature.\n             */\n            disconnectAudioMonitor: undefined,\n            pttReleaseTimeout: undefined,\n            sourceCameraStream: null,\n            sourceScreenStream: null,\n        });\n        this.blurManager = undefined;\n    }\n\n    start() {\n        const services = this.store.env.services;\n        this.notification = services.notification;\n        this.soundEffectsService = services[\"mail.sound_effects\"];\n        this.pttExtService = services[\"discuss.ptt_extension\"];\n        /**\n         * @type {import(\"@mail/discuss/call/common/peer_to_peer\").PeerToPeer}\n         */\n        this.p2pService = services[\"discuss.p2p\"];\n        onChange(this.store.settings, \"useBlur\", () => {\n            if (this.state.sendCamera) {\n                this.toggleVideo(\"camera\", true);\n            }\n        });\n        onChange(this.store.settings, [\"edgeBlurAmount\", \"backgroundBlurAmount\"], () => {\n            if (this.blurManager) {\n                this.blurManager.edgeBlur = this.store.settings.edgeBlurAmount;\n                this.blurManager.backgroundBlur = this.store.settings.backgroundBlurAmount;\n            }\n        });\n        onChange(this.store.settings, [\"voiceActivationThreshold\", \"use_push_to_talk\"], () => {\n            this.linkVoiceActivationDebounce();\n        });\n        onChange(this.store.settings, \"audioInputDeviceId\", async () => {\n            if (this.selfSession) {\n                await this.resetAudioTrack({ force: true });\n            }\n        });\n        this.store.env.bus.addEventListener(\"RTC-SERVICE:PLAY_MEDIA\", () => {\n            for (const session of this.state.channel.rtcSessions) {\n                session.playAudio();\n            }\n        });\n        browser.addEventListener(\n            \"keydown\",\n            (ev) => {\n                if (!this.store.settings.isPushToTalkKey(ev)) {\n                    return;\n                }\n                this.onPushToTalk();\n            },\n            { capture: true }\n        );\n        browser.addEventListener(\n            \"keyup\",\n            (ev) => {\n                if (\n                    !this.state.channel ||\n                    !this.store.settings.use_push_to_talk ||\n                    !this.store.settings.isPushToTalkKey(ev) ||\n                    !this.selfSession.isTalking\n                ) {\n                    return;\n                }\n                this.setPttReleaseTimeout();\n            },\n            { capture: true }\n        );\n\n        browser.addEventListener(\"pagehide\", () => {\n            if (this.state.channel) {\n                const data = JSON.stringify({\n                    params: { channel_id: this.state.channel.id },\n                });\n                const blob = new Blob([data], { type: \"application/json\" });\n                // using sendBeacon allows sending a post request even when the\n                // browser prevents async requests from firing when the browser\n                // is closed. Alternatives like synchronous XHR are not reliable.\n                browser.navigator.sendBeacon(\"/mail/rtc/channel/leave_call\", blob);\n                this.sfuClient?.disconnect();\n            }\n        });\n        /**\n         * Call all sessions for which no peerConnection is established at\n         * a regular interval to try to recover any connection that failed\n         * to start.\n         *\n         * This is distinct from this.recover which tries to restore\n         * connections that were established but failed or timed out.\n         */\n        browser.setInterval(async () => {\n            if (!this.selfSession || !this.state.channel) {\n                return;\n            }\n            await this.ping();\n            if (!this.selfSession || !this.state.channel) {\n                return;\n            }\n            this.call();\n        }, 30_000);\n    }\n\n    setPttReleaseTimeout(duration = 200) {\n        this.state.pttReleaseTimeout = browser.setTimeout(() => {\n            this.setTalking(false);\n            if (!this.selfSession?.isMute) {\n                this.soundEffectsService.play(\"push-to-talk-off\", { volume: 0.3 });\n            }\n        }, Math.max(this.store.settings.voice_active_duration || 0, duration));\n    }\n\n    onPushToTalk() {\n        if (\n            !this.state.channel ||\n            this.store.settings.isRegisteringKey ||\n            !this.store.settings.use_push_to_talk\n        ) {\n            return;\n        }\n        browser.clearTimeout(this.state.pttReleaseTimeout);\n        if (!this.selfSession.isTalking && !this.selfSession.isMute) {\n            this.soundEffectsService.play(\"push-to-talk-on\", { volume: 0.3 });\n        }\n        this.setTalking(true);\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {any} param0.id\n     * @param {string} param0.text\n     * @param {number} [param0.delay]\n     */\n    addCallNotification({ id, text, delay = 3000 }) {\n        if (this.notifications.has(id)) {\n            return;\n        }\n        this.notifications.set(id, { id, text });\n        this.timeouts.set(\n            id,\n            browser.setTimeout(() => {\n                this.notifications.delete(id);\n                this.timeouts.delete(id);\n            }, delay)\n        );\n    }\n\n    /**\n     * @param {any} id\n     */\n    removeCallNotification(id) {\n        browser.clearTimeout(this.timeouts.get(id));\n        this.notifications.delete(id);\n        this.timeouts.delete(id);\n    }\n\n    /**\n     * Notifies the server and does the cleanup of the current call.\n     */\n    async leaveCall(channel = this.state.channel) {\n        this.state.hasPendingRequest = true;\n        await this.rpcLeaveCall(channel);\n        this.endCall(channel);\n        this.state.hasPendingRequest = false;\n    }\n\n    /**\n     * @param {import(\"models\").Thread} [channel]\n     */\n    endCall(channel = this.state.channel) {\n        channel.rtcInvitingSession = undefined;\n        channel.activeRtcSession = undefined;\n        if (channel.eq(this.state.channel)) {\n            this.pttExtService.unsubscribe();\n            this.network?.disconnect();\n            this.clear();\n            this.soundEffectsService.play(\"channel-leave\");\n        }\n    }\n\n    async deafen() {\n        await this.setDeaf(true);\n        this.soundEffectsService.play(\"deafen\");\n    }\n\n    /**\n     * @param {import(\"@mail/discuss/call/common/rtc_session_model\").RtcSession} session\n     * @param {boolean} active\n     */\n    setRemoteRaiseHand(session, active) {\n        if (Boolean(session.raisingHand) === active) {\n            return;\n        }\n        Object.assign(session, {\n            raisingHand: active ? new Date() : undefined,\n        });\n        const notificationId = \"raise_hand_\" + session.id;\n        if (session.raisingHand) {\n            this.addCallNotification({\n                id: notificationId,\n                text: _t(\"%s raised their hand\", session.name),\n            });\n        } else {\n            this.removeCallNotification(notificationId);\n        }\n    }\n\n    async mute() {\n        await this.setMute(true);\n        this.soundEffectsService.play(\"mute\");\n    }\n\n    /**\n     * @param {import(\"models\").Thread} channel\n     * @param {Object} [initialState={}]\n     * @param {boolean} [initialState.audio]\n     * @param {boolean} [initialState.camera]\n     */\n    async toggleCall(channel, { audio = true, camera } = {}) {\n        if (this.state.hasPendingRequest) {\n            return;\n        }\n        const isActiveCall = channel.eq(this.state.channel);\n        if (this.state.channel) {\n            await this.leaveCall(this.state.channel);\n        }\n        if (!isActiveCall) {\n            await this.joinCall(channel, { audio, camera });\n        }\n    }\n\n    async toggleMicrophone() {\n        if (this.selfSession.isMute) {\n            await this.unmute();\n        } else {\n            await this.mute();\n        }\n    }\n\n    async undeafen() {\n        await this.setDeaf(false);\n        this.soundEffectsService.play(\"undeafen\");\n    }\n\n    async unmute() {\n        if (this.state.audioTrack) {\n            await this.setMute(false);\n        } else {\n            await this.resetAudioTrack({ force: true });\n        }\n        this.soundEffectsService.play(\"unmute\");\n    }\n\n    //----------------------------------------------------------------------\n    // Private\n    //----------------------------------------------------------------------\n\n    async _loadSfu() {\n        const load = async () => {\n            await loadSfuAssets();\n            const sfuModule = odoo.loader.modules.get(\"@mail/../lib/odoo_sfu/odoo_sfu\");\n            this.SFU_CLIENT_STATE = sfuModule.SFU_CLIENT_STATE;\n            this.sfuClient = new sfuModule.SfuClient();\n        };\n        try {\n            await load();\n        } catch {\n            // trying again with a delay in case of race condition with the asset loading.\n            await new Promise((resolve, reject) => {\n                browser.setTimeout(async () => {\n                    try {\n                        await load();\n                    } catch (error) {\n                        reject(error);\n                    }\n                    resolve();\n                }, 1000);\n            });\n        }\n    }\n\n    async _initConnection() {\n        this.state.connectionType = CONNECTION_TYPES.P2P;\n        this.network?.disconnect();\n        // loading p2p in any case as we may need to receive peer-to-peer connections from users who failed to connect to the SFU.\n        this.p2pService.connect(this.selfSession.id, this.state.channel.id, {\n            info: this.formatInfo(),\n            iceServers: this.iceServers,\n        });\n        this.network = new Network(this.p2pService);\n        if (this.serverInfo) {\n            try {\n                await this._loadSfu();\n                this.state.connectionType = CONNECTION_TYPES.SERVER;\n                this.network.addSfu(this.sfuClient);\n            } catch (e) {\n                this.notification.add(\n                    _t(\"Failed to load the SFU server, falling back to peer-to-peer\"),\n                    {\n                        type: \"warning\",\n                    }\n                );\n                this.log(this.selfSession, \"failed to load sfu server\", { error: e });\n            }\n        }\n        this.network.addEventListener(\"stateChange\", this._handleSfuClientStateChange);\n        this.network.addEventListener(\"update\", this._handleNetworkUpdates);\n        this.network.addEventListener(\"log\", ({ detail: { id, level, message } }) => {\n            const session = this.store.RtcSession.get(id);\n            if (session) {\n                this.log(session, message, { step: \"p2p\", level });\n            }\n        });\n    }\n\n    /**\n     * @param {RtcSession} session\n     * @param {String} entry\n     * @param {Object} [param2]\n     * @param {Error} [param2.error]\n     * @param {String} [param2.step] current step of the flow\n     * @param {String} [param2.state] current state of the connection\n     */\n    log(session, entry, { error, step, state, ...data } = {}) {\n        session.logStep = entry;\n        if (!this.store.settings.logRtc) {\n            return;\n        }\n        if (!this.state.logs.has(session.id)) {\n            this.state.logs.set(session.id, { step: \"\", state: \"\", logs: [] });\n        }\n        if (step) {\n            this.state.logs.get(session.id).step = step;\n        }\n        if (state) {\n            this.state.logs.get(session.id).state = state;\n        }\n        const trace = window.Error().stack || \"\";\n        this.state.logs.get(session.id).logs.push({\n            event: `${luxon.DateTime.now().toFormat(\"HH:mm:ss\")}: ${entry}`,\n            error: error && {\n                name: error.name,\n                message: error.message,\n                stack: error.stack && error.stack.split(\"\\n\"),\n            },\n            trace: trace.split(\"\\n\"),\n            ...data,\n        });\n    }\n\n    /**\n     * @param {CustomEvent} param0\n     * @param {Object} param0.detail\n     * @param {String} param0.detail.name\n     * @param {any} param0.detail.payload\n     */\n    async _handleNetworkUpdates({ detail: { name, payload } }) {\n        if (!this.state.channel) {\n            return;\n        }\n        switch (name) {\n            case \"connection_change\":\n                {\n                    const { id, state } = payload;\n                    const session = this.store.RtcSession.get(id);\n                    if (!session) {\n                        return;\n                    }\n                    session.connectionState = state;\n                }\n                return;\n            case \"disconnect\":\n                {\n                    const { sessionId } = payload;\n                    const session = this.store.RtcSession.get(sessionId);\n                    if (!session) {\n                        return;\n                    }\n                    this.disconnect(session);\n                }\n                return;\n            case \"info_change\":\n                if (!payload) {\n                    return;\n                }\n                for (const [id, info] of Object.entries(payload)) {\n                    const session = this.store.RtcSession.get(Number(id));\n                    if (!session) {\n                        return;\n                    }\n                    // `isRaisingHand` is turned into the Date `raisingHand`\n                    this.setRemoteRaiseHand(session, info.isRaisingHand);\n                    delete info.isRaisingHand;\n                    Object.assign(session, info);\n                }\n                return;\n            case \"track\":\n                {\n                    const { sessionId, type, track, active } = payload;\n                    const session = this.store.RtcSession.get(sessionId);\n                    if (!session) {\n                        return;\n                    }\n                    try {\n                        await this.handleRemoteTrack({ session, track, type, active });\n                    } catch {\n                        // ignored, the session may be closing.\n                        // this can happen when you join a call from another tab in which you have another session.\n                    }\n                    // makes sure we are not downloading a video that is not displayed\n                    setTimeout(() => {\n                        this.updateVideoDownload(session);\n                    }, 2000);\n                }\n                return;\n        }\n    }\n\n    async _handleSfuClientStateChange({ detail: { state, cause } }) {\n        this.state.serverState = state;\n        switch (state) {\n            case this.SFU_CLIENT_STATE.AUTHENTICATED:\n                // if we are hot-swapping connection type, we clear the p2p as late as possible\n                this.p2pService.removeALlPeers();\n                this.selfSession.connectionState = \"connecting\";\n                break;\n            case this.SFU_CLIENT_STATE.CONNECTED:\n                this.sfuClient.updateInfo(this.formatInfo(), {\n                    needRefresh: true, // asks the server to send the info from all the channel\n                });\n                this.sfuClient.updateUpload(\"audio\", this.state.audioTrack);\n                this.sfuClient.updateUpload(\"camera\", this.state.cameraTrack);\n                this.sfuClient.updateUpload(\"screen\", this.state.screenTrack);\n                this.selfSession.connectionState = \"connected\";\n                return;\n            case this.SFU_CLIENT_STATE.CLOSED:\n                {\n                    let text;\n                    if (cause === \"full\") {\n                        text = _t(\"Channel full\");\n                    } else {\n                        text = _t(\"Connection to SFU server closed by the server\");\n                    }\n                    this.notification.add(text, {\n                        type: \"warning\",\n                    });\n                    await this.leaveCall();\n                }\n                return;\n        }\n    }\n\n    async call() {\n        if (this.state.connectionType === CONNECTION_TYPES.SERVER) {\n            if (this.sfuClient.state === this.SFU_CLIENT_STATE.DISCONNECTED) {\n                await this.sfuClient.connect(this.serverInfo.url, this.serverInfo.jsonWebToken, {\n                    channelUUID: this.serverInfo.channelUUID,\n                    iceServers: this.iceServers,\n                });\n            }\n            return;\n        }\n        if (this.state.channel.rtcSessions.length === 0) {\n            return;\n        }\n        for (const session of this.state.channel.rtcSessions) {\n            if (session.eq(this.selfSession)) {\n                continue;\n            }\n            this.log(session, \"init call\", { step: \"init call\" });\n            this.p2pService.addPeer(session.id);\n        }\n    }\n\n    /**\n     * @param {import(\"@mail/discuss/call/common/rtc_session_model\").RtcSession} session\n     * @param {MediaStreamTrack} track\n     * @param {streamType} type\n     * @param {boolean} active false if the track is muted/disabled\n     */\n    async handleRemoteTrack({ session, track, type, active = true }) {\n        session.updateStreamState(type, active);\n        await this.updateStream(session, track, {\n            mute: this.selfSession.isDeaf,\n            videoType: type,\n        });\n        this.updateActiveSession(session, type, { addVideo: true });\n    }\n\n    /**\n     * @param {import(\"models\").Thread} channel\n     * @param {object} [initialState]\n     * @param {boolean} [initialState.audio] whether to request and use the user audio input (microphone) at start\n     * @param {boolean} [initialState.camera] whether to request and use the user video input (camera) at start\n     */\n    async joinCall(channel, { audio = true, camera = false } = {}) {\n        if (!IS_CLIENT_RTC_COMPATIBLE) {\n            this.notification.add(_t(\"Your browser does not support webRTC.\"), { type: \"warning\" });\n            return;\n        }\n        this.pttExtService.subscribe();\n        this.state.hasPendingRequest = true;\n        const data = await rpc(\n            \"/mail/rtc/channel/join_call\",\n            {\n                channel_id: channel.id,\n                check_rtc_session_ids: channel.rtcSessions.map((session) => session.id),\n            },\n            { silent: true }\n        );\n        // Initializing a new session implies closing the current session.\n        this.clear();\n        this.state.logs.clear();\n        this.state.channel = channel;\n        this.store.insert(data);\n        this.state.logs.set(\"channelId\", this.state.channel.id);\n        this.state.logs.set(\"selfSessionId\", this.selfSession.id);\n        this.state.logs.set(\"hasTURN\", hasTurn(this.iceServers));\n        const channelProxy = reactive(this.state.channel, () => {\n            if (channel.notEq(this.state.channel)) {\n                throw new Error(\"channel has changed\");\n            }\n            if (this.state.channel) {\n                if (this.state.channel && this.selfSession.notIn(channelProxy.rtcSessions)) {\n                    // if the current RTC session is not in the channel sessions, this call is no longer valid.\n                    this.endCall();\n                    return;\n                }\n                for (const session of this.state.channel.rtcSessions) {\n                    if (session.notIn(channelProxy.rtcSessions)) {\n                        this.log(session, \"session removed from the server\");\n                        this.disconnect(session);\n                    }\n                }\n            }\n            void channelProxy.rtcSessions.map((s) => s);\n        });\n        this.state.updateAndBroadcastDebounce = debounce(\n            async () => {\n                if (!this.selfSession) {\n                    return;\n                }\n                await rpc(\n                    \"/mail/rtc/session/update_and_broadcast\",\n                    {\n                        session_id: this.selfSession.id,\n                        values: {\n                            is_camera_on: this.selfSession.isCameraOn,\n                            is_deaf: this.selfSession.isDeaf,\n                            is_muted: this.selfSession.isSelfMuted,\n                            is_screen_sharing_on: this.selfSession.isScreenSharingOn,\n                        },\n                    },\n                    { silent: true }\n                );\n            },\n            3000,\n            { leading: true, trailing: true }\n        );\n        this.state.channel.rtcInvitingSession = undefined;\n        await this._initConnection();\n        if (!this.state.channel?.id) {\n            return;\n        }\n        await this.call();\n        if (!this.state.channel?.id) {\n            return;\n        }\n        this.soundEffectsService.play(\"channel-join\");\n        this.state.hasPendingRequest = false;\n        await this.resetAudioTrack({ force: audio });\n        if (!this.state.channel?.id) {\n            return;\n        }\n        if (camera) {\n            await this.toggleVideo(\"camera\");\n        }\n    }\n\n    async rpcLeaveCall(channel) {\n        await rpc(\n            \"/mail/rtc/channel/leave_call\",\n            {\n                channel_id: channel.id,\n            },\n            { silent: true }\n        );\n    }\n\n    async ping() {\n        const data = await rpc(\n            \"/discuss/channel/ping\",\n            {\n                channel_id: this.state.channel.id,\n                check_rtc_session_ids: this.state.channel.rtcSessions.map((session) => session.id),\n                rtc_session_id: this.selfSession.id,\n            },\n            { silent: true }\n        );\n        this.store.insert(data);\n    }\n\n    disconnect(session) {\n        const downloadTimeout = this.downloadTimeouts.get(session.id);\n        if (downloadTimeout) {\n            clearTimeout(downloadTimeout);\n            this.downloadTimeouts.delete(session.id);\n        }\n        this.removeCallNotification(\"raise_hand_\" + session.id);\n        session.raisingHand = undefined;\n        session.logStep = undefined;\n        session.audioError = undefined;\n        session.videoError = undefined;\n        session.connectionState = undefined;\n        session.isTalking = false;\n        session.mainVideoStreamType = undefined;\n        this.removeAudioFromSession(session);\n        this.removeVideoFromSession(session);\n        this.p2pService?.removePeer(session.id);\n        this.log(session, \"peer removed\", { step: \"peer removed\" });\n    }\n\n    clear() {\n        if (this.state.channel) {\n            for (const session of this.state.channel.rtcSessions) {\n                this.removeAudioFromSession(session);\n                this.removeVideoFromSession(session);\n                session.isTalking = false;\n            }\n        }\n        this.sfuClient = undefined;\n        this.network = undefined;\n        this.state.serverState = undefined;\n        this.state.updateAndBroadcastDebounce?.cancel();\n        this.state.disconnectAudioMonitor?.();\n        this.state.audioTrack?.stop();\n        this.state.cameraTrack?.stop();\n        this.state.screenTrack?.stop();\n        closeStream(this.state.sourceCameraStream);\n        this.state.sourceCameraStream = null;\n        if (this.blurManager) {\n            this.blurManager.close();\n            this.blurManager = undefined;\n        }\n        this.update({\n            selfSession: undefined,\n            serverInfo: undefined,\n        });\n        Object.assign(this.state, {\n            updateAndBroadcastDebounce: undefined,\n            connectionType: undefined,\n            disconnectAudioMonitor: undefined,\n            cameraTrack: undefined,\n            screenTrack: undefined,\n            audioTrack: undefined,\n            sendCamera: false,\n            sendScreen: false,\n            channel: undefined,\n        });\n    }\n\n    /**\n     * @param {Boolean} isDeaf\n     */\n    async setDeaf(isDeaf) {\n        this.updateAndBroadcast({ isDeaf });\n        for (const session of this.state.channel.rtcSessions) {\n            if (!session.audioElement) {\n                continue;\n            }\n            session.audioElement.muted = isDeaf;\n        }\n        await this.refreshAudioStatus();\n    }\n\n    /**\n     * @param {Boolean} isSelfMuted\n     */\n    async setMute(isSelfMuted) {\n        this.updateAndBroadcast({ isSelfMuted });\n        await this.refreshAudioStatus();\n    }\n\n    /**\n     * @param {Boolean} raise\n     */\n    async raiseHand(raise) {\n        if (!this.selfSession || !this.state.channel) {\n            return;\n        }\n        this.selfSession.raisingHand = raise ? new Date() : undefined;\n        await this.network?.updateInfo(this.formatInfo());\n    }\n\n    /**\n     * @param {boolean} isTalking\n     */\n    async setTalking(isTalking) {\n        if (!this.selfSession || isTalking === this.selfSession.isTalking) {\n            return;\n        }\n        this.selfSession.isTalking = isTalking;\n        if (!this.selfSession.isMute) {\n            this.pttExtService.notifyIsTalking(isTalking);\n            await this.refreshAudioStatus();\n        }\n    }\n\n    /**\n     * @param {string} type\n     * @param {boolean} [force]\n     */\n    async toggleVideo(type, force) {\n        if (!this.state.channel?.id) {\n            return;\n        }\n        switch (type) {\n            case \"camera\": {\n                const track = this.state.cameraTrack;\n                const sendCamera = force ?? !this.state.sendCamera;\n                this.state.sendCamera = false;\n                await this.setVideo(track, type, sendCamera);\n                break;\n            }\n            case \"screen\": {\n                const track = this.state.screenTrack;\n                const sendScreen = force ?? !this.state.sendScreen;\n                this.state.sendScreen = false;\n                await this.setVideo(track, type, sendScreen);\n                break;\n            }\n        }\n        if (this.selfSession) {\n            switch (type) {\n                case \"camera\": {\n                    this.removeVideoFromSession(this.selfSession, \"camera\");\n                    if (this.state.cameraTrack) {\n                        this.updateStream(this.selfSession, this.state.cameraTrack);\n                    }\n                    break;\n                }\n                case \"screen\": {\n                    if (!this.state.screenTrack) {\n                        this.removeVideoFromSession(this.selfSession, \"screen\");\n                    } else {\n                        this.updateStream(this.selfSession, this.state.screenTrack);\n                    }\n                    break;\n                }\n            }\n        }\n        const updatedTrack = type === \"camera\" ? this.state.cameraTrack : this.state.screenTrack;\n        await this.network?.updateUpload(type, updatedTrack);\n        if (!this.selfSession) {\n            return;\n        }\n        switch (type) {\n            case \"camera\": {\n                this.updateAndBroadcast({\n                    isCameraOn: !!this.state.sendCamera,\n                });\n                break;\n            }\n            case \"screen\": {\n                this.updateAndBroadcast({\n                    isScreenSharingOn: !!this.state.sendScreen,\n                });\n                break;\n            }\n        }\n    }\n\n    updateAndBroadcast(data) {\n        const session = this.selfSession;\n        Object.assign(session, data);\n        this.state.updateAndBroadcastDebounce?.();\n    }\n\n    /**\n     * Sets the enabled property of the local audio track based on the\n     * current session state. And notifies peers of the new audio state.\n     */\n    async refreshAudioStatus() {\n        if (!this.state.audioTrack) {\n            return;\n        }\n        this.state.audioTrack.enabled = !this.selfSession.isMute && this.selfSession.isTalking;\n        this.network?.updateInfo(this.formatInfo());\n    }\n\n    /**\n     * @param {String} type 'camera' or 'screen'\n     */\n    async setVideo(track, type, activateVideo = false) {\n        if (this.blurManager) {\n            this.blurManager.close();\n            this.blurManager = undefined;\n        }\n        const stopVideo = () => {\n            if (track) {\n                track.stop();\n            }\n            switch (type) {\n                case \"camera\": {\n                    this.state.cameraTrack = undefined;\n                    closeStream(this.state.sourceCameraStream);\n                    this.state.sourceCameraStream = null;\n                    break;\n                }\n                case \"screen\": {\n                    this.state.screenTrack = undefined;\n                    closeStream(this.state.sourceScreenStream);\n                    this.state.sourceScreenStream = null;\n                    break;\n                }\n            }\n        };\n        if (!activateVideo) {\n            if (type === \"screen\") {\n                this.soundEffectsService.play(\"screen-sharing\");\n            }\n            stopVideo();\n            return;\n        }\n        let sourceStream;\n        try {\n            if (type === \"camera\") {\n                if (this.state.sourceCameraStream && this.state.sendCamera) {\n                    sourceStream = this.state.sourceCameraStream;\n                } else {\n                    sourceStream = await browser.navigator.mediaDevices.getUserMedia({\n                        video: CAMERA_CONFIG,\n                    });\n                }\n            }\n            if (type === \"screen\") {\n                if (this.state.sourceScreenStream && this.state.sendScreen) {\n                    sourceStream = this.state.sourceScreenStream;\n                } else {\n                    sourceStream = await browser.navigator.mediaDevices.getDisplayMedia({\n                        video: SCREEN_CONFIG,\n                    });\n                }\n                this.soundEffectsService.play(\"screen-sharing\");\n            }\n        } catch {\n            const str =\n                type === \"camera\"\n                    ? _t('%s\" requires \"camera\" access', window.location.host)\n                    : _t('%s\" requires \"screen recording\" access', window.location.host);\n            this.notification.add(str, { type: \"warning\" });\n            stopVideo();\n            return;\n        }\n        let videoStream = sourceStream;\n        if (this.store.settings.useBlur && type === \"camera\") {\n            try {\n                this.blurManager = new BlurManager(sourceStream, {\n                    backgroundBlur: this.store.settings.backgroundBlurAmount,\n                    edgeBlur: this.store.settings.edgeBlurAmount,\n                });\n                videoStream = await this.blurManager.stream;\n            } catch (_e) {\n                this.notification.add(\n                    _t(\"%(name)s: %(message)s)\", { name: _e.name, message: _e.message }),\n                    { type: \"warning\" }\n                );\n                this.store.settings.useBlur = false;\n            }\n        }\n        track = videoStream ? videoStream.getVideoTracks()[0] : undefined;\n        if (track) {\n            track.addEventListener(\"ended\", async () => {\n                await this.toggleVideo(type, false);\n            });\n        }\n        switch (type) {\n            case \"camera\": {\n                Object.assign(this.state, {\n                    sourceCameraStream: sourceStream,\n                    cameraTrack: track,\n                    sendCamera: Boolean(track),\n                });\n                break;\n            }\n            case \"screen\": {\n                Object.assign(this.state, {\n                    sourceScreenStream: sourceStream,\n                    screenTrack: track,\n                    sendScreen: Boolean(track),\n                });\n                break;\n            }\n        }\n    }\n\n    async resetAudioTrack({ force = false }) {\n        if (this.state.audioTrack) {\n            this.state.audioTrack.stop();\n            this.state.audioTrack = undefined;\n        }\n        if (!this.state.channel) {\n            return;\n        }\n        if (this.selfSession) {\n            this.setMute(true);\n        }\n        if (force) {\n            let audioTrack;\n            try {\n                const audioStream = await browser.navigator.mediaDevices.getUserMedia({\n                    audio: this.store.settings.audioConstraints,\n                });\n                audioTrack = audioStream.getAudioTracks()[0];\n                if (this.selfSession) {\n                    this.setMute(false);\n                }\n            } catch {\n                this.notification.add(\n                    _t('\"%(hostname)s\" requires microphone access', {\n                        hostname: window.location.host,\n                    }),\n                    { type: \"warning\" }\n                );\n                return;\n            }\n            if (!this.selfSession) {\n                // The getUserMedia promise could resolve when the call is ended\n                // in which case the track is no longer relevant.\n                audioTrack.stop();\n                return;\n            }\n            audioTrack.addEventListener(\"ended\", async () => {\n                // this mostly happens when the user retracts microphone permission.\n                await this.resetAudioTrack({ force: false });\n                this.setMute(true);\n            });\n            audioTrack.enabled = !this.selfSession.isMute && this.selfSession.isTalking;\n            this.state.audioTrack = audioTrack;\n            this.linkVoiceActivationDebounce();\n            await this.network.updateUpload(\"audio\", this.state.audioTrack);\n        }\n    }\n\n    /**\n     * Updates the way broadcast of the local audio track is handled,\n     * attaches an audio monitor for voice activation if necessary.\n     */\n    async linkVoiceActivation() {\n        this.state.disconnectAudioMonitor?.();\n        if (!this.selfSession) {\n            return;\n        }\n        if (this.store.settings.use_push_to_talk || !this.state.channel || !this.state.audioTrack) {\n            this.selfSession.isTalking = false;\n            await this.refreshAudioStatus();\n            return;\n        }\n        try {\n            this.state.disconnectAudioMonitor = await monitorAudio(this.state.audioTrack, {\n                onThreshold: async (isAboveThreshold) => {\n                    this.setTalking(isAboveThreshold);\n                },\n                volumeThreshold: this.store.settings.voiceActivationThreshold,\n            });\n        } catch {\n            /**\n             * The browser is probably missing audioContext,\n             * in that case, voice activation is not enabled\n             * and the microphone is always 'on'.\n             */\n            this.notification.add(_t(\"Your browser does not support voice activation\"), {\n                type: \"warning\",\n            });\n            this.selfSession.isTalking = true;\n        }\n        await this.refreshAudioStatus();\n    }\n\n    /**\n     * @param {import(\"models\").id} id\n     */\n    deleteSession(id) {\n        const session = this.store.RtcSession.get(id);\n        if (session) {\n            if (this.selfSession && session.eq(this.selfSession)) {\n                this.endCall();\n            }\n            this.disconnect(session);\n            session.delete();\n        }\n    }\n\n    formatInfo() {\n        this.selfSession.isCameraOn = Boolean(this.state.cameraTrack);\n        this.selfSession.isScreenSharingOn = Boolean(this.state.screenTrack);\n        return this.selfSession.info;\n    }\n\n    /**\n     * @param {RtcSession} session\n     * @param {MediaStreamTrack} track\n     * @param {Object} [parm1]\n     * @param {boolean} [parm1.mute]\n     * @param {\"camera\"|\"screen\"} [parm1.videoType]\n     */\n    async updateStream(session, track, { mute, videoType } = {}) {\n        const stream = new window.MediaStream();\n        stream.addTrack(track);\n        if (track.kind === \"audio\") {\n            const audioElement = session.audioElement || new window.Audio();\n            audioElement.srcObject = stream;\n            audioElement.load();\n            audioElement.muted = mute;\n            audioElement.volume = this.store.settings.getVolume(session);\n            // Using both autoplay and play() as safari may prevent play() outside of user interactions\n            // while some browsers may not support or block autoplay.\n            audioElement.autoplay = true;\n            session.audioElement = audioElement;\n            session.audioStream = stream;\n            session.isSelfMuted = false;\n            session.isTalking = false;\n            await session.playAudio();\n        }\n        if (track.kind === \"video\") {\n            videoType = videoType\n                ? videoType\n                : track.id === this.state.cameraTrack?.id\n                ? \"camera\"\n                : \"screen\";\n            session.videoStreams.set(videoType, stream);\n            this.updateActiveSession(session, videoType, { addVideo: true });\n        }\n    }\n\n    /**\n     * @param {RtcSession} session\n     * @param {String} [videoType]\n     */\n    removeVideoFromSession(session, videoType = false) {\n        if (videoType) {\n            this.updateActiveSession(session, videoType);\n            closeStream(session.videoStreams.get(videoType));\n            session.videoStreams.delete(videoType);\n        } else {\n            for (const stream of session.videoStreams.values()) {\n                closeStream(stream);\n            }\n            session.videoStreams.clear();\n        }\n    }\n    /**\n     * @param {RtcSession} session\n     */\n    removeAudioFromSession(session) {\n        closeStream(session.audioStream);\n        if (session.audioElement) {\n            session.audioElement.pause();\n            try {\n                session.audioElement.srcObject = undefined;\n            } catch {\n                // ignore error during remove, the value will be overwritten at next usage anyway\n            }\n        }\n        session.audioStream = undefined;\n    }\n\n    /**\n     * @param {RtcSession} session\n     * @param {\"screen\"|\"camera\"} [videoType]\n     * @param {Object} [parm2]\n     * @param {boolean} [parm2.addVideo]\n     */\n    updateActiveSession(session, videoType, { addVideo = false } = {}) {\n        const activeRtcSession = this.state.channel.activeRtcSession;\n        if (addVideo) {\n            if (videoType === \"screen\") {\n                this.state.channel.activeRtcSession = session;\n                session.mainVideoStreamType = videoType;\n                return;\n            }\n            if (activeRtcSession && session.hasVideo && !session.isMainVideoStreamActive) {\n                session.mainVideoStreamType = videoType;\n            }\n            return;\n        }\n        if (!activeRtcSession || activeRtcSession.notEq(session)) {\n            return;\n        }\n        if (activeRtcSession.isMainVideoStreamActive) {\n            if (videoType === session.mainVideoStreamType) {\n                if (videoType === \"screen\") {\n                    this.state.channel.activeRtcSession = undefined;\n                } else {\n                    session.mainVideoStreamType = \"screen\";\n                }\n            }\n        }\n    }\n\n    /**\n     * @param {import(\"@mail/discuss/call/common/rtc_session_model\").RtcSession} rtcSession\n     * @param {Object} [param1]\n     * @param {number} [param1.viewCountIncrement=0] negative value to decrement\n     */\n    updateVideoDownload(rtcSession, { viewCountIncrement = 0 } = {}) {\n        rtcSession.videoComponentCount += viewCountIncrement;\n        const downloadTimeout = this.downloadTimeouts.get(rtcSession.id);\n        if (downloadTimeout) {\n            this.downloadTimeouts.delete(rtcSession.id);\n            browser.clearTimeout(downloadTimeout);\n        }\n        if (rtcSession.videoComponentCount > 0) {\n            this.network?.updateDownload(rtcSession.id, {\n                camera: true,\n                screen: true,\n            });\n        } else {\n            /**\n             * We wait a bit before pausing a download to avoid flickering, if the user stops downloading and starts again\n             * soon after, it is not worth pausing the download.\n             */\n            this.downloadTimeouts.set(\n                rtcSession.id,\n                browser.setTimeout(() => {\n                    this.downloadTimeouts.delete(rtcSession.id);\n                    this.network?.updateDownload(rtcSession.id, {\n                        camera: false,\n                        screen: false,\n                    });\n                }, 1000)\n            );\n        }\n    }\n}\n\nRtc.register();\n\nexport const rtcService = {\n    dependencies: [\n        \"bus_service\",\n        \"discuss.p2p\",\n        \"discuss.ptt_extension\",\n        \"mail.sound_effects\",\n        \"mail.store\",\n        \"notification\",\n    ],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    start(env, services) {\n        const rtc = env.services[\"mail.store\"].rtc;\n        rtc.p2pService = services[\"discuss.p2p\"];\n        services[\"bus_service\"].subscribe(\n            \"discuss.channel.rtc.session/sfu_hot_swap\",\n            async ({ serverInfo }) => {\n                if (!rtc.selfSession) {\n                    return;\n                }\n                if (rtc.serverInfo?.url === serverInfo?.url) {\n                    // no reason to swap if the server is the same, if at some point we want to force a swap\n                    // there should be an explicit flag in the event payload.\n                    return;\n                }\n                rtc.serverInfo = serverInfo;\n                await rtc._initConnection();\n                await rtc.call();\n            }\n        );\n        services[\"bus_service\"].subscribe(\"discuss.channel.rtc.session/ended\", ({ sessionId }) => {\n            if (rtc.selfSession?.id === sessionId) {\n                rtc.endCall();\n                services.notification.add(_t(\"Disconnected from the RTC call by the server\"), {\n                    type: \"warning\",\n                });\n            }\n        });\n        services[\"bus_service\"].subscribe(\"res.users.settings.volumes\", (payload) => {\n            if (payload) {\n                rtc.store.Volume.insert(payload);\n            }\n        });\n        services[\"bus_service\"].subscribe(\n            \"discuss.channel.rtc.session/update_and_broadcast\",\n            (payload) => {\n                const { data, channelId } = payload;\n                /**\n                 * If this event comes from the channel of the current call, information is shared in real time\n                 * through the peer to peer connection. So we do not use this less accurate broadcast.\n                 */\n                if (channelId !== rtc.state.channel?.id) {\n                    rtc.store.insert(data);\n                }\n            }\n        );\n        return rtc;\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.rtc\", rtcService);\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class RtcSession extends Record {\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").RtcSession>} */\n    static records = {};\n    /** @returns {import(\"models\").RtcSession} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").RtcSession|import(\"models\").RtcSession[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n    static _insert() {\n        /** @type {import(\"models\").RtcSession} */\n        const session = super._insert(...arguments);\n        session.channel?.rtcSessions.add(session);\n        return session;\n    }\n\n    // Server data\n    /** @type {boolean} */\n    channelMember = Record.one(\"ChannelMember\", { inverse: \"rtcSession\" });\n    /** @type {boolean} */\n    isCameraOn;\n    /** @type {boolean} */\n    isScreenSharingOn;\n    /** @type {number} */\n    id;\n    /** @type {boolean} */\n    isDeaf;\n    /** @type {boolean} */\n    isSelfMuted;\n    // Client data\n    /** @type {HTMLAudioElement} */\n    audioElement;\n    /** @type {MediaStream} */\n    audioStream;\n    /** @type {RTCDataChannel} */\n    dataChannel;\n    audioError;\n    videoError;\n    isTalking = Record.attr(false, {\n        /** @this {import(\"models\").RtcSession} */\n        onUpdate() {\n            if (this.isTalking && !this.isMute) {\n                this.talkingTime = this.store.nextTalkingTime++;\n            }\n        },\n    });\n    isActuallyTalking = Record.attr(false, {\n        /** @this {import(\"models\").RtcSession} */\n        compute() {\n            return this.isTalking && !this.isMute;\n        },\n    });\n    isVideoStreaming = Record.attr(false, {\n        /** @this {import(\"models\").RtcSession} */\n        compute() {\n            return this.isScreenSharingOn || this.isCameraOn;\n        },\n    });\n    shortStatus = Record.attr(undefined, {\n        compute() {\n            if (this.isScreenSharingOn) {\n                return \"live\";\n            }\n            if (this.isDeaf) {\n                return \"deafen\";\n            }\n            if (this.isMute) {\n                return \"mute\";\n            }\n        },\n    });\n    talkingTime = 0;\n    localVolume;\n    /** @type {RTCPeerConnection} */\n    peerConnection;\n    /** @type {Date|undefined} */\n    raisingHand;\n    videoComponentCount = 0;\n    /** @type {Map<'screen'|'camera', MediaStream>} */\n    videoStreams = new Map();\n    /** @type {string} */\n    mainVideoStreamType;\n    // RTC stats\n    connectionState;\n    localCandidateType;\n    remoteCandidateType;\n    dataChannelState;\n    packetsReceived;\n    packetsSent;\n    dtlsState;\n    iceState;\n    iceGatheringState;\n    logStep;\n\n    get channel() {\n        return this.channelMember?.thread;\n    }\n\n    get isMute() {\n        return this.isSelfMuted || this.isDeaf;\n    }\n\n    get mainVideoStream() {\n        return this.isMainVideoStreamActive && this.videoStreams.get(this.mainVideoStreamType);\n    }\n\n    get isMainVideoStreamActive() {\n        if (!this.mainVideoStreamType) {\n            return false;\n        }\n        return this.mainVideoStreamType === \"camera\" ? this.isCameraOn : this.isScreenSharingOn;\n    }\n\n    get hasVideo() {\n        return this.isScreenSharingOn || this.isCameraOn;\n    }\n\n    getStream(type) {\n        const isActive = type === \"camera\" ? this.isCameraOn : this.isScreenSharingOn;\n        return isActive && this.videoStreams.get(type);\n    }\n\n    /**\n     * @returns {{isSelfMuted: boolean, isDeaf: boolean, isTalking: boolean, isRaisingHand: boolean}}\n     */\n    get info() {\n        return {\n            isSelfMuted: this.isSelfMuted,\n            isRaisingHand: Boolean(this.raisingHand),\n            isDeaf: this.isDeaf,\n            isTalking: this.isTalking,\n            isCameraOn: this.isCameraOn,\n            isScreenSharingOn: this.isScreenSharingOn,\n        };\n    }\n\n    get partnerId() {\n        const persona = this.channelMember?.persona;\n        return persona.type === \"partner\" ? persona.id : undefined;\n    }\n\n    get guestId() {\n        const persona = this.channelMember?.persona;\n        return persona.type === \"guest\" ? persona.id : undefined;\n    }\n\n    /**\n     * @returns {string}\n     */\n    get name() {\n        return this.channelMember?.persona.name;\n    }\n\n    /**\n     * @returns {number} float\n     */\n    get volume() {\n        return this.audioElement?.volume || this.localVolume;\n    }\n\n    set volume(value) {\n        if (this.audioElement) {\n            this.audioElement.volume = value;\n        }\n        this.localVolume = value;\n    }\n\n    async playAudio() {\n        if (!this.audioElement) {\n            return;\n        }\n        try {\n            await this.audioElement.play();\n            this.audioError = undefined;\n        } catch (error) {\n            this.audioError = error.name;\n        }\n    }\n\n    /**\n     * @param {\"audio\" | \"camera\" | \"screen\"} type\n     * @param {boolean} state\n     */\n    updateStreamState(type, state) {\n        if (type === \"camera\") {\n            this.isCameraOn = state;\n        } else if (type === \"screen\") {\n            this.isScreenSharingOn = state;\n        }\n    }\n}\n\nRtcSession.register();\n", "import { Settings } from \"@mail/core/common/settings_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Settings} */\nconst SettingsPatch = {\n    setup() {\n        super.setup(...arguments);\n    },\n    getVolume(rtcSession) {\n        return (\n            rtcSession.volume ||\n            this.volumes.find(\n                (volume) =>\n                    (volume.type === \"partner\" && volume.persona.id === rtcSession.partnerId) ||\n                    (volume.type === \"guest\" && volume.persona.id === rtcSession.guestId)\n            )?.volume ||\n            0.5\n        );\n    },\n};\npatch(Settings.prototype, SettingsPatch);\n", "import { Record } from \"@mail/core/common/record\";\nimport { Store } from \"@mail/core/common/store_service\";\nimport { RtcSession } from \"@mail/discuss/call/common/rtc_session_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Store} */\nconst StorePatch = {\n    setup() {\n        super.setup(...arguments);\n        /** @type {typeof import(\"@mail/discuss/call/common/rtc_session_model\").RtcSession} */\n        this.RtcSession = RtcSession;\n        this.rtc = Record.one(\"Rtc\", {\n            compute() {\n                return {};\n            },\n        });\n        this.ringingThreads = Record.many(\"Thread\", {\n            /** @this {import(\"models\").Store} */\n            onUpdate() {\n                if (this.ringingThreads.length > 0) {\n                    this.env.services[\"mail.sound_effects\"].play(\"incoming-call\", {\n                        loop: true,\n                    });\n                } else {\n                    this.env.services[\"mail.sound_effects\"].stop(\"incoming-call\");\n                }\n            },\n        });\n        this.allActiveRtcSessions = Record.many(\"RtcSession\");\n        this.nextTalkingTime = 1;\n    },\n    onStarted() {\n        super.onStarted(...arguments);\n        this.rtc.start();\n    },\n    sortMembers(m1, m2) {\n        const m1HasRtc = Boolean(m1.rtcSession);\n        const m2HasRtc = Boolean(m2.rtcSession);\n        if (m1HasRtc === m2HasRtc) {\n            /**\n             * If raisingHand is falsy, it gets an Infinity value so that when\n             * we sort by [oldest/lowest-value]-first, falsy values end up last.\n             */\n            const m1RaisingValue = m1.rtcSession?.raisingHand || Infinity;\n            const m2RaisingValue = m2.rtcSession?.raisingHand || Infinity;\n            if (m1HasRtc && m1RaisingValue !== m2RaisingValue) {\n                return m1RaisingValue - m2RaisingValue;\n            } else {\n                return super.sortMembers(m1, m2);\n            }\n        } else {\n            return m2HasRtc - m1HasRtc;\n        }\n    },\n};\npatch(Store.prototype, StorePatch);\n", "import { threadActionsRegistry } from \"@mail/core/common/thread_actions\";\nimport { CallSettings } from \"@mail/discuss/call/common/call_settings\";\n\nimport { useComponent, useState } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nthreadActionsRegistry\n    .add(\"call\", {\n        condition(component) {\n            return (\n                component.thread?.allowCalls && !component.thread?.eq(component.rtc.state.channel)\n            );\n        },\n        icon: \"fa fa-fw fa-phone\",\n        iconLarge: \"fa fa-fw fa-lg fa-phone\",\n        name: _t(\"Start a Call\"),\n        open(component) {\n            component.rtc.toggleCall(component.thread);\n        },\n        sequence: 10,\n        sequenceQuick: 30,\n        setup() {\n            const component = useComponent();\n            component.rtc = useState(useService(\"discuss.rtc\"));\n        },\n    })\n    .add(\"settings\", {\n        component: CallSettings,\n        componentProps(action) {\n            return { isCompact: true };\n        },\n        condition(component) {\n            return (\n                component.thread?.allowCalls &&\n                (component.props.chatWindow?.isOpen || component.store.inPublicPage)\n            );\n        },\n        icon: \"fa fa-fw fa-gear\",\n        iconLarge: \"fa fa-fw fa-lg fa-gear\",\n        name: _t(\"Call Settings\"),\n        sequence: 20,\n        sequenceGroup: 30,\n        setup() {\n            const component = useComponent();\n            component.rtc = useState(useService(\"discuss.rtc\"));\n        },\n        toggle: true,\n    });\n", "import { Record } from \"@mail/core/common/record\";\nimport { Thread } from \"@mail/core/common/thread_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Thread} */\nconst ThreadPatch = {\n    setup() {\n        super.setup(...arguments);\n        this.activeRtcSession = Record.one(\"RtcSession\", {\n            /** @this {import(\"models\").Thread} */\n            onAdd(r) {\n                this.store.allActiveRtcSessions.add(r);\n            },\n            /** @this {import(\"models\").Thread} */\n            onDelete(r) {\n                this.store.allActiveRtcSessions.delete(r);\n            },\n        });\n        this.hadSelfSession = false;\n        this.lastSessionIds = new Set();\n        this.rtcInvitingSession = Record.one(\"RtcSession\", {\n            /** @this {import(\"models\").Thread} */\n            onAdd(r) {\n                this.rtcSessions.add(r);\n                this.store.ringingThreads.add(this);\n            },\n            /** @this {import(\"models\").Thread} */\n            onDelete(r) {\n                this.store.ringingThreads.delete(this);\n            },\n        });\n        this.rtcSessions = Record.many(\"RtcSession\", {\n            /** @this {import(\"models\").Thread} */\n            onDelete(r) {\n                this.store.env.services[\"discuss.rtc\"].deleteSession(r.id);\n            },\n            /** @this {import(\"models\").Thread} */\n            onUpdate() {\n                const hadSelfSession = this.hadSelfSession;\n                const lastSessionIds = this.lastSessionIds;\n                this.hadSelfSession = Boolean(this.store.rtc.selfSession?.in(this.rtcSessions));\n                this.lastSessionIds = new Set(this.rtcSessions.map((s) => s.id));\n                if (\n                    !hadSelfSession || // sound for self-join is played instead\n                    !this.hadSelfSession || // sound for self-leave is played instead\n                    !this.store.env.services[\"multi_tab\"].isOnMainTab() // another tab playing sound\n                ) {\n                    return;\n                }\n                if ([...this.lastSessionIds].some((id) => !lastSessionIds.has(id))) {\n                    this.store.env.services[\"mail.sound_effects\"].play(\"channel-join\");\n                }\n                if ([...lastSessionIds].some((id) => !this.lastSessionIds.has(id))) {\n                    this.store.env.services[\"mail.sound_effects\"].play(\"member-leave\");\n                }\n            },\n        });\n    },\n    get videoCount() {\n        return Object.values(this.store.RtcSession.records).filter((session) => session.hasVideo)\n            .length;\n    },\n};\npatch(Thread.prototype, ThreadPatch);\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { Typing } from \"@mail/discuss/typing/common/typing\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useDebounced } from \"@web/core/utils/timing\";\n\nconst commandRegistry = registry.category(\"discuss.channel_commands\");\n\nexport const SHORT_TYPING = 5000;\nexport const LONG_TYPING = 50000;\n\npatch(Composer, {\n    components: { ...Composer.components, Typing },\n});\n\npatch(Composer.prototype, {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        this.typingNotified = false;\n        this.stopTypingDebounced = useDebounced(this.stopTyping.bind(this), SHORT_TYPING);\n    },\n    /**\n     * Notify the server of the current typing status\n     *\n     * @param {boolean} [is_typing=true]\n     */\n    notifyIsTyping(is_typing = true) {\n        if (this.thread?.model === \"discuss.channel\" && this.thread.id > 0) {\n            rpc(\n                \"/discuss/channel/notify_typing\",\n                {\n                    channel_id: this.thread.id,\n                    is_typing,\n                },\n                { silent: true }\n            );\n        }\n    },\n    detectTyping() {\n        const value = this.props.composer.text;\n        if (this.thread?.model === \"discuss.channel\" && value.startsWith(\"/\")) {\n            const [firstWord] = value.substring(1).split(/\\s/);\n            const command = commandRegistry.get(firstWord, false);\n            if (\n                value === \"/\" || // suggestions not yet started\n                this.hasSuggestions ||\n                (command &&\n                    (!command.channel_types ||\n                        command.channel_types.includes(this.thread.channel_type)))\n            ) {\n                this.stopTyping();\n                return;\n            }\n        }\n        if (!this.typingNotified && value) {\n            this.typingNotified = true;\n            this.notifyIsTyping();\n            browser.setTimeout(() => (this.typingNotified = false), LONG_TYPING);\n        }\n        this.stopTypingDebounced();\n    },\n    /**\n     * @override\n     */\n    async sendMessage() {\n        await super.sendMessage();\n        this.stopTyping();\n    },\n    stopTyping() {\n        if (this.typingNotified) {\n            this.typingNotified = false;\n            this.notifyIsTyping(false);\n        }\n    },\n    addEmoji(str) {\n        super.addEmoji(str);\n        this.detectTyping();\n    },\n});\n", "import { ThreadIcon } from \"@mail/core/common/thread_icon\";\nimport { Typing } from \"@mail/discuss/typing/common/typing\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ThreadIcon, {\n    components: { ...ThreadIcon.components, Typing },\n});\n", "import { Component } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Thread} channel\n * @property {string} [size]\n * @property {boolean} [displayText]\n * @extends {Component<Props, Env>}\n */\nexport class Typing extends Component {\n    static defaultProps = {\n        size: \"small\",\n        displayText: true,\n    };\n    static props = [\"channel?\", \"size?\", \"displayText?\", \"member?\"];\n    static template = \"discuss.Typing\";\n\n    /** @returns {string} */\n    get text() {\n        const typingMemberNames = this.props.member\n            ? [this.props.member.name]\n            : this.props.channel.otherTypingMembers.map(({ name }) => name);\n        if (typingMemberNames.length === 1) {\n            return _t(\"%s is typing...\", typingMemberNames[0]);\n        }\n        if (typingMemberNames.length === 2) {\n            return _t(\"%(user1)s and %(user2)s are typing...\", {\n                user1: typingMemberNames[0],\n                user2: typingMemberNames[1],\n            });\n        }\n        return _t(\"%(user1)s, %(user2)s and more are typing...\", {\n            user1: typingMemberNames[0],\n            user2: typingMemberNames[1],\n        });\n    }\n}\n", "const { DateTime } = luxon;\n\n/**\n * @param {luxon.DateTime} datetime\n */\nexport function computeDelay(datetime) {\n    if (!datetime) {\n        return 0;\n    }\n    const today = DateTime.now().startOf(\"day\");\n    return datetime.diff(today, \"days\").days;\n}\n\nexport function getMsToTomorrow() {\n    const now = new Date();\n    const night = new Date(\n        now.getFullYear(),\n        now.getMonth(),\n        now.getDate() + 1, // the next day\n        0,\n        0,\n        0 // at 00:00:00 hours\n    );\n    return night.getTime() - now.getTime();\n}\n\nexport function isToday(datetime) {\n    if (!datetime) {\n        return false;\n    }\n    return (\n        datetime.toLocaleString(DateTime.DATE_FULL) ===\n        DateTime.now().toLocaleString(DateTime.DATE_FULL)\n    );\n}\n", "import { stateToUrl } from \"@web/core/browser/router\";\nimport { loadEmoji } from \"@web/core/emoji_picker/emoji_picker\";\n\nimport { escape, unaccent } from \"@web/core/utils/strings\";\n\nconst urlRegexp =\n    /\\b(?:https?:\\/\\/\\d{1,3}(?:\\.\\d{1,3}){3}|(?:https?:\\/\\/|(?:www\\.))[-a-z0-9@:%._+~#=\\u00C0-\\u024F\\u1E00-\\u1EFF]{2,256}\\.[a-z]{2,13})\\b(?:[-a-z0-9@:%_+~#?&[\\]^|{}`\\\\'$//=\\u00C0-\\u024F\\u1E00-\\u1EFF]|[.]*[-a-z0-9@:%_+~#?&[\\]^|{}`\\\\'$//=\\u00C0-\\u024F\\u1E00-\\u1EFF]|,(?!$| )|\\.(?!$| |\\.)|;(?!$| ))*/gi;\n\n/**\n * Escape < > & as html entities\n *\n * @param {string}\n * @return {string}\n */\nconst _escapeEntities = (function () {\n    const map = { \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\" };\n    const escaper = function (match) {\n        return map[match];\n    };\n    const testRegexp = RegExp(\"(?:&|<|>)\");\n    const replaceRegexp = RegExp(\"(?:&|<|>)\", \"g\");\n    return function (string) {\n        string = string == null ? \"\" : \"\" + string;\n        return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;\n    };\n})();\n\n/**\n * @param rawBody {string}\n * @param validRecords {Object}\n * @param validRecords.partners {Partner}\n */\nexport async function prettifyMessageContent(rawBody, validRecords = []) {\n    // Suggested URL Javascript regex of http://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url\n    // Adapted to make http(s):// not required if (and only if) www. is given. So `should.notmatch` does not match.\n    // And further extended to include Latin-1 Supplement, Latin Extended-A, Latin Extended-B and Latin Extended Additional.\n    const escapedAndCompactContent = escapeAndCompactTextContent(rawBody);\n    let body = escapedAndCompactContent.replace(/&nbsp;/g, \" \").trim();\n    // This message will be received from the mail composer as html content\n    // subtype but the urls will not be linkified. If the mail composer\n    // takes the responsibility to linkify the urls we end up with double\n    // linkification a bit everywhere. Ideally we want to keep the content\n    // as text internally and only make html enrichment at display time but\n    // the current design makes this quite hard to do.\n    body = generateMentionsLinks(body, validRecords);\n    body = parseAndTransform(body, addLink);\n    body = await _generateEmojisOnHtml(body);\n    return body;\n}\n\n/**\n * WARNING: this is not enough to unescape potential XSS contained in htmlString, transformFunction\n * should handle it or it should be handled after/before calling parseAndTransform. So if the result\n * of this function is used in a t-raw, be very careful.\n *\n * @param {string} htmlString\n * @param {function} transformFunction\n * @returns {string}\n */\nexport function parseAndTransform(htmlString, transformFunction) {\n    const openToken = \"OPEN\" + Date.now();\n    const string = htmlString.replace(/&lt;/g, openToken);\n    let children;\n    try {\n        const div = document.createElement(\"div\");\n        div.innerHTML = string; // /!\\ quotes are unescaped\n        children = Array.from(div.childNodes);\n    } catch {\n        const div = document.createElement(\"div\");\n        div.innerHTML = `<pre>${string}</pre>`;\n        children = Array.from(div.childNodes);\n    }\n    return _parseAndTransform(children, transformFunction).replace(\n        new RegExp(openToken, \"g\"),\n        \"&lt;\"\n    );\n}\n\n/**\n * @param {Node[]} nodes\n * @param {function} transformFunction with:\n *   param node\n *   param function\n *   return string\n * @return {string}\n */\nfunction _parseAndTransform(nodes, transformFunction) {\n    if (!nodes) {\n        return;\n    }\n    return Object.values(nodes)\n        .map((node) => {\n            return transformFunction(node, function () {\n                return _parseAndTransform(node.childNodes, transformFunction);\n            });\n        })\n        .join(\"\");\n}\n\n/**\n * @param {string} text\n * @return {string} linkified text\n */\nfunction linkify(text) {\n    let curIndex = 0;\n    let result = \"\";\n    let match;\n    while ((match = urlRegexp.exec(text)) !== null) {\n        result += _escapeEntities(text.slice(curIndex, match.index));\n        // Decode the url first, in case it's already an encoded url\n        const url = decodeURI(match[0]);\n        const href = encodeURI(!/^https?:\\/\\//i.test(url) ? \"http://\" + url : url);\n        result += `<a target=\"_blank\" rel=\"noreferrer noopener\" href=\"${href}\">${_escapeEntities(\n            url\n        )}</a>`;\n        curIndex = match.index + match[0].length;\n    }\n    return result + _escapeEntities(text.slice(curIndex));\n}\n\nexport function addLink(node, transformChildren) {\n    if (node.nodeType === 3) {\n        // text node\n        const linkified = linkify(node.data);\n        if (linkified !== node.data) {\n            const div = document.createElement(\"div\");\n            div.innerHTML = linkified;\n            for (const childNode of [...div.childNodes]) {\n                node.parentNode.insertBefore(childNode, node);\n            }\n            node.parentNode.removeChild(node);\n            return linkified;\n        }\n        return node.textContent;\n    }\n    if (node.tagName === \"A\") {\n        return node.outerHTML;\n    }\n    transformChildren();\n    return node.outerHTML;\n}\n\n/**\n * Returns an escaped conversion of a content.\n *\n * @param {string} content\n * @returns {string}\n */\nexport function escapeAndCompactTextContent(content) {\n    //Removing unwanted extra spaces from message\n    let value = escape(content).trim();\n    value = value.replace(/(\\r|\\n){2,}/g, \"<br/><br/>\");\n    value = value.replace(/(\\r|\\n)/g, \"<br/>\");\n\n    // prevent html space collapsing\n    value = value.replace(/ /g, \"&nbsp;\").replace(/([^>])&nbsp;([^<])/g, \"$1 $2\");\n    return value;\n}\n\n/**\n * @param body {string}\n * @param validRecords {Object}\n * @param validRecords.partners {Array}\n * @return {string}\n */\nfunction generateMentionsLinks(body, { partners = [], threads = [], specialMentions = [] }) {\n    const mentions = [];\n    for (const partner of partners) {\n        const placeholder = `@-mention-partner-${partner.id}`;\n        const text = `@${escape(partner.name)}`;\n        mentions.push({\n            class: \"o_mail_redirect\",\n            id: partner.id,\n            model: \"res.partner\",\n            placeholder,\n            text,\n        });\n        body = body.replace(text, placeholder);\n    }\n    for (const thread of threads) {\n        const placeholder = `#-mention-channel-${thread.id}`;\n        let className, text;\n        if (thread.parent_channel_id) {\n            className = \"o_channel_redirect o_channel_redirect_asThread\";\n            text = escape(`#${thread.parent_channel_id.displayName} > ${thread.displayName}`);\n        } else {\n            className = \"o_channel_redirect\";\n            text = escape(`#${thread.displayName}`);\n        }\n        mentions.push({\n            class: className,\n            id: thread.id,\n            model: \"discuss.channel\",\n            placeholder,\n            text,\n        });\n        body = body.replace(text, placeholder);\n    }\n    for (const special of specialMentions) {\n        body = body.replace(\n            `@${escape(special)}`,\n            `<a href=\"#\" class=\"o-discuss-mention\">@${escape(special)}</a>`\n        );\n    }\n    for (const mention of mentions) {\n        const href = `href='${stateToUrl({ model: mention.model, resId: mention.id })}'`;\n        const attClass = `class='${mention.class}'`;\n        const dataOeId = `data-oe-id='${mention.id}'`;\n        const dataOeModel = `data-oe-model='${mention.model}'`;\n        const target = \"target='_blank'\";\n        const link = `<a ${href} ${attClass} ${dataOeId} ${dataOeModel} ${target} contenteditable=\"false\">${mention.text}</a>`;\n        body = body.replace(mention.placeholder, link);\n    }\n    return body;\n}\n\n/**\n * @private\n * @param {string} htmlString\n * @returns {string}\n */\nasync function _generateEmojisOnHtml(htmlString) {\n    const { emojis } = await loadEmoji();\n    for (const emoji of emojis) {\n        for (const source of [...emoji.shortcodes, ...emoji.emoticons]) {\n            const escapedSource = String(source).replace(/([.*+?=^!:${}()|[\\]/\\\\])/g, \"\\\\$1\");\n            const regexp = new RegExp(\"(\\\\s|^)(\" + escapedSource + \")(?=\\\\s|$)\", \"g\");\n            htmlString = htmlString.replace(regexp, \"$1\" + emoji.codepoints);\n        }\n    }\n    return htmlString;\n}\n\nexport function htmlToTextContentInline(htmlString) {\n    const fragment = document.createDocumentFragment();\n    const div = document.createElement(\"div\");\n    fragment.appendChild(div);\n    htmlString = htmlString.replace(/<br\\s*\\/?>/gi, \" \");\n    try {\n        div.innerHTML = htmlString;\n    } catch {\n        div.innerHTML = `<pre>${htmlString}</pre>`;\n    }\n    return div.textContent\n        .trim()\n        .replace(/[\\n\\r]/g, \"\")\n        .replace(/\\s\\s+/g, \" \");\n}\n\nexport function convertBrToLineBreak(str) {\n    return new DOMParser().parseFromString(\n        str.replaceAll(\"<br>\", \"\\n\").replaceAll(\"</br>\", \"\\n\"),\n        \"text/html\"\n    ).body.textContent;\n}\n\nexport function cleanTerm(term) {\n    return unaccent((typeof term === \"string\" ? term : \"\").toLowerCase());\n}\n\n/**\n * Parses text to find email: Tagada <address@mail.fr> -> [Tagada, address@mail.fr] or False\n *\n * @param {string} text\n * @returns {[string,string|boolean]|false}\n */\nexport function parseEmail(text) {\n    if (!text) {\n        return;\n    }\n    let result = text.match(/\"?(.*?)\"? <(.*@.*)>/);\n    if (result) {\n        const name = (result[1] || \"\").trim().replace(/(^\"|\"$)/g, \"\");\n        return [name, (result[2] || \"\").trim()];\n    }\n    result = text.match(/(.*@.*)/);\n    if (result) {\n        return [String(result[1] || \"\").trim(), String(result[1] || \"\").trim()];\n    }\n    return [text, false];\n}\n\nexport const EMOJI_REGEX = /\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F|\\u200d/gu;\n", "import {\n    onMounted,\n    onPatched,\n    onWillUnmount,\n    useComponent,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { makeDraggableHook } from \"@web/core/utils/draggable_hook_builder_owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport function useLazyExternalListener(target, eventName, handler, eventParams) {\n    const boundHandler = handler.bind(useComponent());\n    let t;\n    onMounted(() => {\n        t = target();\n        if (!t) {\n            return;\n        }\n        t.addEventListener(eventName, boundHandler, eventParams);\n    });\n    onPatched(() => {\n        const t2 = target();\n        if (t !== t2) {\n            if (t) {\n                t.removeEventListener(eventName, boundHandler, eventParams);\n            }\n            if (t2) {\n                t2.addEventListener(eventName, boundHandler, eventParams);\n            }\n            t = t2;\n        }\n    });\n    onWillUnmount(() => {\n        if (!t) {\n            return;\n        }\n        t.removeEventListener(eventName, boundHandler, eventParams);\n    });\n}\n\nexport function onExternalClick(refName, cb) {\n    let downTarget, upTarget;\n    const ref = useRef(refName);\n    function onClick(ev) {\n        if (ref.el && !ref.el.contains(ev.composedPath()[0])) {\n            cb(ev, { downTarget, upTarget });\n            upTarget = downTarget = null;\n        }\n    }\n    function onMousedown(ev) {\n        downTarget = ev.target;\n    }\n    function onMouseup(ev) {\n        upTarget = ev.target;\n    }\n    onMounted(() => {\n        document.body.addEventListener(\"mousedown\", onMousedown, true);\n        document.body.addEventListener(\"mouseup\", onMouseup, true);\n        document.body.addEventListener(\"click\", onClick, true);\n    });\n    onWillUnmount(() => {\n        document.body.removeEventListener(\"mousedown\", onMousedown, true);\n        document.body.removeEventListener(\"mouseup\", onMouseup, true);\n        document.body.removeEventListener(\"click\", onClick, true);\n    });\n}\n\n/**\n * @param {string | string[]} refNames name of refs that determine whether this is in state \"hovering\".\n *   ref name that end with \"*\" means it takes parented HTML node into account too. Useful for floating\n *   menu where dropdown menu container is not accessible.\n * @param {Object} param1\n * @param {() => void} [param1.onHover] callback when hovering the ref names.\n * @param {() => void} [param1.onAway] callback when stop hovering the ref names.\n * @param {number, () => void} [param1.onHovering] array where 1st param is duration until start hovering\n *   and function to be executed at this delay duration after hovering is kept true.\n * @returns {({ isHover: boolean })}\n */\nexport function useHover(refNames, { onHover, onAway, onHovering } = {}) {\n    refNames = Array.isArray(refNames) ? refNames : [refNames];\n    const targets = [];\n    let wasHovering = false;\n    let hoveringTimeout;\n    let awayTimeout;\n    for (const refName of refNames) {\n        targets.push({\n            ref: refName.endsWith(\"*\")\n                ? useRef(refName.substring(0, refName.length - 1))\n                : useRef(refName),\n        });\n    }\n    const state = useState({\n        set isHover(newIsHover) {\n            if (this._isHover !== newIsHover) {\n                this._isHover = newIsHover;\n                this._count++;\n            }\n        },\n        get isHover() {\n            void this._count;\n            return this._isHover;\n        },\n        _count: 0,\n        _isHover: false,\n    });\n    function setHover(hovering) {\n        if (hovering && !wasHovering) {\n            state.isHover = true;\n            clearTimeout(awayTimeout);\n            clearTimeout(hoveringTimeout);\n            if (typeof onHover === \"function\") {\n                onHover();\n            }\n            if (Array.isArray(onHovering)) {\n                const [delay, cb] = onHovering;\n                hoveringTimeout = setTimeout(() => {\n                    cb();\n                }, delay);\n            }\n        } else if (!hovering) {\n            state.isHover = false;\n            clearTimeout(awayTimeout);\n            if (typeof onAway === \"function\") {\n                awayTimeout = setTimeout(() => {\n                    clearTimeout(hoveringTimeout);\n                    onAway();\n                }, 200);\n            }\n        }\n        wasHovering = hovering;\n    }\n    function onmouseenter(ev) {\n        if (state.isHover) {\n            return;\n        }\n        for (const target of targets) {\n            if (!target.ref.el) {\n                continue;\n            }\n            if (target.ref.el.contains(ev.target)) {\n                setHover(true);\n                return;\n            }\n        }\n    }\n    function onmouseleave(ev) {\n        if (!state.isHover) {\n            return;\n        }\n        for (const target of targets) {\n            if (!target.ref.el) {\n                continue;\n            }\n            if (target.ref.el.contains(ev.relatedTarget)) {\n                return;\n            }\n        }\n        setHover(false);\n    }\n\n    for (const target of targets) {\n        useLazyExternalListener(\n            () => target.ref.el,\n            \"mouseenter\",\n            (ev) => onmouseenter(ev),\n            true\n        );\n        useLazyExternalListener(\n            () => target.ref.el,\n            \"mouseleave\",\n            (ev) => onmouseleave(ev),\n            true\n        );\n    }\n    return state;\n}\n\n/**\n * Hook that execute the callback function each time the scrollable element hit\n * the bottom minus the threshold.\n *\n * @param {string} refName scrollable t-ref name to observe\n * @param {function} callback function to execute when scroll hit the bottom minus the threshold\n * @param {number} threshold number of threshold pixel to trigger the callback\n */\nexport function useOnBottomScrolled(refName, callback, threshold = 1) {\n    const ref = useRef(refName);\n    function onScroll() {\n        if (Math.abs(ref.el.scrollTop + ref.el.clientHeight - ref.el.scrollHeight) < threshold) {\n            callback();\n        }\n    }\n    onMounted(() => {\n        ref.el.addEventListener(\"scroll\", onScroll);\n    });\n    onWillUnmount(() => {\n        ref.el.removeEventListener(\"scroll\", onScroll);\n    });\n}\n\n/**\n * @param {string} refName\n * @param {function} cb\n */\nexport function useVisible(refName, cb, { ready = true } = {}) {\n    const ref = useRef(refName);\n    const state = useState({\n        isVisible: undefined,\n        ready,\n    });\n    function setValue(value) {\n        state.isVisible = value;\n        cb(state.isVisible);\n    }\n    const observer = new IntersectionObserver((entries) => {\n        setValue(entries.at(-1).isIntersecting);\n    });\n    useEffect(\n        (el, ready) => {\n            if (el && ready) {\n                observer.observe(el);\n                return () => {\n                    setValue(undefined);\n                    observer.unobserve(el);\n                };\n            }\n        },\n        () => [ref.el, state.ready]\n    );\n    return state;\n}\n\n/**\n * @typedef {Object} MessageHighlight\n * @property {function} clearHighlight\n * @property {function} highlightMessage\n * @property {number|null} highlightedMessageId\n * @returns {MessageHighlight}\n */\nexport function useMessageHighlight(duration = 2000) {\n    let timeout;\n    const state = useState({\n        clearHighlight() {\n            if (this.highlightedMessageId) {\n                browser.clearTimeout(timeout);\n                timeout = null;\n                this.highlightedMessageId = null;\n            }\n        },\n        /**\n         * @param {import(\"models\").Message} message\n         * @param {import(\"models\").Thread} thread\n         */\n        async highlightMessage(message, thread) {\n            if (thread.notEq(message.thread)) {\n                return;\n            }\n            await thread.loadAround(message.id);\n            const lastHighlightedMessageId = state.highlightedMessageId;\n            this.clearHighlight();\n            if (lastHighlightedMessageId === message.id) {\n                // Give some time for the state to update.\n                await new Promise(setTimeout);\n            }\n            thread.scrollTop = undefined;\n            state.highlightedMessageId = message.id;\n            timeout = browser.setTimeout(() => this.clearHighlight(), duration);\n        },\n        scrollPromise: null,\n        /**\n         * Scroll the element into view and expose a promise that will resolved\n         * once the scroll is done.\n         *\n         * @param {Element} el\n         */\n        scrollTo(el) {\n            state.scrollPromise?.resolve();\n            const scrollPromise = new Deferred();\n            state.scrollPromise = scrollPromise;\n            if (\"onscrollend\" in window) {\n                document.addEventListener(\"scrollend\", scrollPromise.resolve, {\n                    capture: true,\n                    once: true,\n                });\n            } else {\n                // To remove when safari will support the \"scrollend\" event.\n                setTimeout(scrollPromise.resolve, 250);\n            }\n            el.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n            return scrollPromise;\n        },\n        highlightedMessageId: null,\n    });\n    return state;\n}\n\nexport function useSelection({ refName, model, preserveOnClickAwayPredicate = () => false }) {\n    const ui = useState(useService(\"ui\"));\n    const ref = useRef(refName);\n    function onSelectionChange() {\n        const activeElement = ref.el?.getRootNode().activeElement;\n        if (activeElement && activeElement === ref.el) {\n            Object.assign(model, {\n                start: ref.el.selectionStart,\n                end: ref.el.selectionEnd,\n                direction: ref.el.selectionDirection,\n            });\n        }\n    }\n    onExternalClick(refName, async (ev) => {\n        if (await preserveOnClickAwayPredicate(ev)) {\n            return;\n        }\n        if (!ref.el) {\n            return;\n        }\n        Object.assign(model, {\n            start: ref.el.value.length,\n            end: ref.el.value.length,\n            direction: ref.el.selectionDirection,\n        });\n    });\n    onMounted(() => {\n        document.addEventListener(\"selectionchange\", onSelectionChange);\n        document.addEventListener(\"input\", onSelectionChange);\n    });\n    onWillUnmount(() => {\n        document.removeEventListener(\"selectionchange\", onSelectionChange);\n        document.removeEventListener(\"input\", onSelectionChange);\n    });\n    return {\n        restore() {\n            ref.el?.setSelectionRange(model.start, model.end, model.direction);\n        },\n        moveCursor(position) {\n            model.start = model.end = position;\n            if (!ui.isSmall) {\n                // In mobile, selection seems to adjust correctly.\n                // Don't programmatically adjust, otherwise it shows soft keyboard!\n                ref.el.selectionStart = ref.el.selectionEnd = position;\n            }\n        },\n    };\n}\n\nexport function useMessageEdition() {\n    const state = useState({\n        /** @type {import('@mail/core/common/composer').Composer} */\n        composerOfThread: null,\n        /** @type {import('@mail/core/common/message_model').Message} */\n        editingMessage: null,\n        exitEditMode() {\n            state.editingMessage = null;\n            if (state.composerOfThread) {\n                state.composerOfThread.props.composer.autofocus++;\n            }\n        },\n    });\n    return state;\n}\n\n/**\n * @typedef {Object} MessageToReplyTo\n * @property {function} cancel\n * @property {function} isNotSelected\n * @property {function} isSelected\n * @property {import(\"models\").Message|null} message\n * @property {import(\"models\").Thread|null} thread\n * @property {function} toggle\n * @returns {MessageToReplyTo}\n */\nexport function useMessageToReplyTo() {\n    return useState({\n        cancel() {\n            Object.assign(this, { message: null, thread: null });\n        },\n        /**\n         * @param {import(\"models\").Thread} thread\n         * @param {import(\"models\").Message} message\n         * @returns {boolean}\n         */\n        isNotSelected(thread, message) {\n            return thread.eq(this.thread) && message.notEq(this.message);\n        },\n        /**\n         * @param {import(\"models\").Thread} thread\n         * @param {import(\"models\").Message} message\n         * @returns {boolean}\n         */\n        isSelected(thread, message) {\n            return thread.eq(this.thread) && message.eq(this.message);\n        },\n        /** @type {import(\"models\").Message|null} */\n        message: null,\n        /** @type {import(\"models\").Thread|null} */\n        thread: null,\n        /**\n         * @param {import(\"models\").Thread} thread\n         * @param {import(\"models\").Message} message\n         */\n        toggle(thread, message) {\n            if (message.eq(this.message)) {\n                this.cancel();\n            } else {\n                Object.assign(this, { message, thread });\n            }\n        },\n    });\n}\n\nexport function useSequential() {\n    let inProgress = false;\n    let nextFunction;\n    let nextResolve;\n    let nextReject;\n    async function call() {\n        const resolve = nextResolve;\n        const reject = nextReject;\n        const func = nextFunction;\n        nextResolve = undefined;\n        nextReject = undefined;\n        nextFunction = undefined;\n        inProgress = true;\n        try {\n            const data = await func();\n            resolve(data);\n        } catch (e) {\n            reject(e);\n        }\n        inProgress = false;\n        if (nextFunction && nextResolve) {\n            call();\n        }\n    }\n    return (func) => {\n        nextResolve?.();\n        const prom = new Promise((resolve, reject) => {\n            nextResolve = resolve;\n            nextReject = reject;\n        });\n        nextFunction = func;\n        if (!inProgress) {\n            call();\n        }\n        return prom;\n    };\n}\n\nexport function useDiscussSystray() {\n    const ui = useState(useService(\"ui\"));\n    return {\n        class: \"o-mail-DiscussSystray-class\",\n        get contentClass() {\n            return `d-flex flex-column flex-grow-1 ${\n                ui.isSmall ? \"overflow-auto w-100 mh-100\" : \"\"\n            }`;\n        },\n        get menuClass() {\n            return `p-0 o-mail-DiscussSystray ${\n                ui.isSmall\n                    ? \"o-mail-systrayFullscreenDropdownMenu start-0 w-100 mh-100 d-flex flex-column mt-0 border-0 shadow-lg\"\n                    : \"\"\n            }`;\n        },\n    };\n}\n\nexport const useMovable = makeDraggableHook({\n    name: \"useMovable\",\n    onWillStartDrag({ ctx, addCleanup, addStyle, getRect }) {\n        const { height } = getRect(ctx.current.element);\n        ctx.current.container = document.createElement(\"div\");\n        addStyle(ctx.current.container, {\n            position: \"fixed\",\n            top: 0,\n            bottom: `${height}px`,\n            left: 0,\n            right: 0,\n        });\n        ctx.current.element.after(ctx.current.container);\n        addCleanup(() => ctx.current.container.remove());\n    },\n    onDrop({ ctx, getRect }) {\n        const { top, left } = getRect(ctx.current.element);\n        return { top, left };\n    },\n});\n", "import { reactive } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport function assignDefined(obj, data, keys = Object.keys(data)) {\n    for (const key of keys) {\n        if (data[key] !== undefined) {\n            obj[key] = data[key];\n        }\n    }\n    return obj;\n}\n\nexport function assignIn(obj, data, keys = Object.keys(data)) {\n    for (const key of keys) {\n        if (key in data) {\n            obj[key] = data[key];\n        }\n    }\n    return obj;\n}\n\n/**\n * @template T\n * @param {T[]} list\n * @param {number} target\n * @param {(item: T) => number} [itemToCompareVal]\n * @returns {T}\n */\nexport function nearestGreaterThanOrEqual(list, target, itemToCompareVal) {\n    const findNext = (left, right, next) => {\n        if (left > right) {\n            return next;\n        }\n        const index = Math.floor((left + right) / 2);\n        const item = list[index];\n        const val = itemToCompareVal?.(item) ?? item;\n        if (val === target) {\n            return item;\n        } else if (val > target) {\n            return findNext(left, index - 1, item);\n        } else {\n            return findNext(index + 1, right, next);\n        }\n    };\n    return findNext(0, list.length - 1, null);\n}\n\nexport const mailGlobal = {\n    isInTest: false,\n};\n\n/**\n * Use `rpc` instead.\n *\n * @deprecated\n */\nexport function rpcWithEnv() {\n    return rpc;\n}\n\n// todo: move this some other place in the future\nexport function isDragSourceExternalFile(dataTransfer) {\n    const dragDataType = dataTransfer.types;\n    if (dragDataType.constructor === window.DOMStringList) {\n        return dragDataType.contains(\"Files\");\n    }\n    if (dragDataType.constructor === Array) {\n        return dragDataType.includes(\"Files\");\n    }\n    return false;\n}\n\n/**\n * @param {Object} target\n * @param {string|string[]} key\n * @param {Function} callback\n */\nexport function onChange(target, key, callback) {\n    let proxy;\n    function _observe() {\n        // access proxy[key] only once to avoid triggering reactive get() many times\n        const val = proxy[key];\n        if (typeof val === \"object\" && val !== null) {\n            void Object.keys(val);\n        }\n        if (Array.isArray(val)) {\n            void val.length;\n            void val.forEach((i) => i);\n        }\n    }\n    if (Array.isArray(key)) {\n        for (const k of key) {\n            onChange(target, k, callback);\n        }\n        return;\n    }\n    proxy = reactive(target, () => {\n        _observe();\n        callback();\n    });\n    _observe();\n    return proxy;\n}\n\n/**\n * @param {MediaStream} [stream]\n */\nexport function closeStream(stream) {\n    stream?.getTracks?.().forEach((track) => track.stop());\n}\n\n/**\n * Compare two Luxon datetime.\n *\n * @param {import(\"@web/core/l10n/dates\").NullableDateTime} date1\n * @param {import(\"@web/core/l10n/dates\").NullableDateTime} date2\n * @returns {number} Negative if date1 is less than date2, positive if date1 is\n *  greater than date2, and 0 if they are equal.\n */\nexport function compareDatetime(date1, date2) {\n    if (date1?.ts === date2?.ts) {\n        return 0;\n    }\n    if (!date1) {\n        return -1;\n    }\n    if (!date2) {\n        return 1;\n    }\n    return date1.ts - date2.ts;\n}\n\n/**\n * Compares two version strings.\n *\n * @param {string} v1 - The first version string to compare.\n * @param {string} v2 - The second version string to compare.\n * @return {number} -1 if v1 is less than v2, 1 if v1 is greater than v2, and 0 if they are equal.\n */\nfunction compareVersion(v1, v2) {\n    const parts1 = v1.split(\".\");\n    const parts2 = v2.split(\".\");\n\n    for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {\n        const num1 = parseInt(parts1[i]) || 0;\n        const num2 = parseInt(parts2[i]) || 0;\n        if (num1 < num2) {\n            return -1;\n        }\n        if (num1 > num2) {\n            return 1;\n        }\n    }\n    return 0;\n}\n\n/**\n * Return a version object that can be compared to other version strings.\n *\n * @param {string} v The version string to evaluate.\n */\nexport function parseVersion(v) {\n    return {\n        isLowerThan(other) {\n            return compareVersion(v, other) < 0;\n        },\n    };\n}\n", "import { Message } from \"@mail/core/common/message_model\";\nimport { Record } from \"@mail/core/common/record\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.rating_id = Record.one(\"rating.rating\");\n    },\n});\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class Rating extends Record {\n    static _name = \"rating.rating\";\n    static id = \"id\";\n\n    /** @type {number} */\n    id;\n    /** @type {number} */\n    rating;\n    /** @type {string} */\n    rating_image_url;\n    /** @type {string} */\n    rating_text;\n}\nRating.register();\n", "import { ChannelMember } from \"@mail/core/common/channel_member_model\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ChannelMember.prototype, {\n    get name() {\n        if (this.thread.channel_type === \"livechat\" && this.persona.user_livechat_username) {\n            return this.persona.user_livechat_username;\n        }\n        if (this.persona.is_public) {\n            return this.thread.anonymous_name;\n        }\n        return super.name;\n    },\n});\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class LivechatChannel extends Record {\n    static _name = \"im_livechat.channel\";\n    static id = \"id\";\n\n    /** @type {boolean} */\n    are_you_inside;\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    name;\n}\nLivechatChannel.register();\n", "import { Message } from \"@mail/core/common/message\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    get authorName() {\n        if (\n            this.message.author?.user_livechat_username &&\n            this.message.thread.channel_type === \"livechat\"\n        ) {\n            return this.message.author.user_livechat_username;\n        }\n        return super.authorName;\n    },\n});\n", "import { Record } from \"@mail/core/common/record\";\nimport { Thread } from \"@mail/core/common/thread_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    setup() {\n        super.setup();\n        this.operator = Record.one(\"Persona\");\n    },\n\n    get typesAllowingCalls() {\n        return super.typesAllowingCalls.concat([\"livechat\"]);\n    },\n\n    get isChatChannel() {\n        return this.channel_type === \"livechat\" || super.isChatChannel;\n    },\n});\n", "import { AttachmentUploadService } from \"@mail/core/common/attachment_upload_service\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(AttachmentUploadService.prototype, {\n    async upload(thread, composer, file, options) {\n        if (thread.channel_type === \"livechat\") {\n            thread = await this.env.services[\"im_livechat.livechat\"].persist();\n            composer = thread.composer;\n            if (!thread) {\n                return;\n            }\n        }\n        return super.upload(thread, composer, file, options);\n    },\n});\n", "import { expirableStorage } from \"@im_livechat/embed/common/expirable_storage\";\nimport { SESSION_STATE } from \"@im_livechat/embed/common/livechat_service\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\n\nexport class AutopopupService {\n    static STORAGE_KEY = \"im_livechat_auto_popup\";\n\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {{\n     * \"im_livechat.chatbot\": import(\"@im_livechat/embed/common/chatbot/chatbot_service\").ChatBotService,\n     * \"im_livechat.initialized\": import(\"@im_livechat/embed/common/livechat_initialized_service\").livechatInitializedService,\n     * \"im_livechat.livechat\": import(\"@im_livechat/embed/common/livechat_service\").LivechatService,\n     * \"mail.store\": import(\"@mail/core/common/store_service\").Store,\n     * ui: typeof import(\"@web/core/ui/ui_service\").uiService.start,\n     * }} services\n     */\n    constructor(\n        env,\n        {\n            \"im_livechat.chatbot\": chatbotService,\n            \"im_livechat.initialized\": livechatInitializedService,\n            \"im_livechat.livechat\": livechatService,\n            \"mail.store\": storeService,\n            ui,\n        }\n    ) {\n        this.storeService = storeService;\n        this.livechatService = livechatService;\n        this.chatbotService = chatbotService;\n        this.ui = ui;\n\n        livechatInitializedService.ready.then(() => {\n            if (this.allowAutoPopup && livechatService.state === SESSION_STATE.NONE) {\n                browser.setTimeout(async () => {\n                    if (!this.storeService.ChatWindow.get({ thread: livechatService.thread })) {\n                        expirableStorage.setItem(AutopopupService.STORAGE_KEY, false);\n                        livechatService.open();\n                    }\n                }, livechatService.rule.auto_popup_timer * 1000);\n            }\n        });\n    }\n\n    get allowAutoPopup() {\n        return Boolean(\n            !expirableStorage.getItem(AutopopupService.STORAGE_KEY) &&\n                !this.ui.isSmall &&\n                this.livechatService.rule?.action === \"auto_popup\" &&\n                this.livechatService.available\n        );\n    }\n}\n\nexport const autoPopupService = {\n    dependencies: [\n        \"im_livechat.livechat\",\n        \"im_livechat.initialized\",\n        \"im_livechat.chatbot\",\n        \"mail.store\",\n        \"ui\",\n    ],\n\n    start(env, services) {\n        return new AutopopupService(env, services);\n    },\n};\nregistry.category(\"services\").add(\"im_livechat.autopopup\", autoPopupService);\n", "import { url } from \"@web/core/utils/urls\";\n\nasync function loadFont(name, url) {\n    await document.fonts.ready;\n    if ([...document.fonts].some(({ family }) => family === name)) {\n        // Font already loaded.\n        return;\n    }\n    const link = document.createElement(\"link\");\n    link.rel = \"preload\";\n    link.as = \"font\";\n    link.href = url;\n    link.crossOrigin = \"\";\n    const style = document.createElement(\"style\");\n    style.appendChild(\n        document.createTextNode(`\n            @font-face {\n                font-family: ${name};\n                src: url('${url}') format('woff2');\n                font-weight: normal;\n                font-style: normal;\n                font-display: block;\n            }\n        `)\n    );\n    const loadPromise = new Promise((res, rej) => {\n        link.addEventListener(\"load\", res);\n        link.addEventListener(\"error\", rej);\n    });\n    document.head.appendChild(link);\n    document.head.appendChild(style);\n    return loadPromise;\n}\n\n/**\n * @param {HTMLElement} target\n * @returns {HTMLDivElement}\n */\nexport function makeRoot(target) {\n    const root = document.createElement(\"div\");\n    root.classList.add(\"o-livechat-root\");\n    root.setAttribute(\"id\", `o-livechat-root-${luxon.DateTime.now().ts + Math.random()}`);\n    root.style.zIndex = \"calc(9e999)\";\n    root.style.position = \"relative\";\n    target.appendChild(root);\n    return root;\n}\n\n/**\n * Initialize the livechat container by loading the styles and\n * the fonts.\n *\n * @param {HTMLElement} root\n * @returns {ShadowRoot}\n */\nexport async function makeShadow(root) {\n    const link = document.createElement(\"link\");\n    link.rel = \"stylesheet\";\n    link.href = url(\"/im_livechat/assets_embed.css\");\n    const stylesLoadedPromise = new Promise((res, rej) => {\n        link.addEventListener(\"load\", res);\n        link.addEventListener(\"error\", rej);\n    });\n    const shadow = root.attachShadow({ mode: \"open\" });\n    shadow.appendChild(link);\n    await Promise.all([\n        stylesLoadedPromise,\n        loadFont(\"FontAwesome\", url(\"/im_livechat/font-awesome\")),\n        loadFont(\"odoo_ui_icons\", url(\"/im_livechat/odoo_ui_icons\")),\n    ]);\n    return shadow;\n}\n", "import { SESSION_STATE } from \"@im_livechat/embed/common/livechat_service\";\nimport { ChatWindow } from \"@mail/core/common/chat_window_model\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ChatWindow.prototype, {\n    setup() {\n        super.setup();\n        this.hasFeedbackPanel = false;\n    },\n    async close() {\n        if (this.thread?.channel_type !== \"livechat\") {\n            return super.close(...arguments);\n        }\n        if (this.store.env.services[\"im_livechat.livechat\"].state === SESSION_STATE.PERSISTED) {\n            this.hasFeedbackPanel = true;\n            this.open({ notifyState: this.thread?.state !== \"open\" });\n        } else {\n            await super.close(...arguments);\n            if (this.thread.isTransient) {\n                this.thread.delete();\n            }\n        }\n        this.store.env.services[\"im_livechat.livechat\"].leave();\n        this.store.env.services[\"im_livechat.chatbot\"].stop();\n    },\n});\n", "import { FeedbackPanel } from \"@im_livechat/embed/common/feedback_panel/feedback_panel\";\nimport { CloseConfirmation } from \"@im_livechat/embed/common/close_confirmation\";\n\nimport { ChatWindow } from \"@mail/core/common/chat_window\";\n\nimport { toRaw, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\n\nObject.assign(ChatWindow.components, { FeedbackPanel, CloseConfirmation });\n\npatch(ChatWindow.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.livechatService = useService(\"im_livechat.livechat\");\n        this.chatbotService = useState(useService(\"im_livechat.chatbot\"));\n        this.livechatState = useState({ showCloseConfirmation: false });\n    },\n\n    async close() {\n        const chatWindow = toRaw(this.props.chatWindow);\n        if (chatWindow.thread.id > 0 && !this.livechatState.showCloseConfirmation) {\n            this.state.actionsDisabled = true;\n            this.livechatState.showCloseConfirmation = true;\n        } else {\n            this.state.actionsDisabled = false;\n            await super.close();\n        }\n    },\n\n    onCloseConfirmationDialog() {\n        this.state.actionsDisabled = false;\n        this.livechatState.showCloseConfirmation = false;\n    },\n});\n", "import { AND, Record } from \"@mail/core/common/record\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { debounce } from \"@web/core/utils/timing\";\n\nexport class Chatbot extends Record {\n    static id = AND(\"script\", \"thread\");\n    static MESSAGE_DELAY = 1500;\n    // Time to wait without user input before considering a multi line step as\n    // completed.\n    static MULTILINE_STEP_DEBOUNCE_DELAY = 10000;\n\n    isTyping = false;\n    script = Record.one(\"ChatbotScript\");\n    currentStep = Record.one(\"ChatbotStep\");\n    steps = Record.many(\"ChatbotStep\");\n    thread = Record.one(\"Thread\", {\n        inverse: \"chatbot\",\n        onDelete() {\n            this.delete();\n        },\n    });\n    typingMessage = Record.one(\"Message\", {\n        compute() {\n            if (this.isTyping && this.thread) {\n                return {\n                    id: -0.1 - this.thread.id,\n                    thread: this.thread,\n                    author: this.script.partner,\n                };\n            }\n        },\n    });\n    /**\n     * @type {(message: import(\"models\").Message) => Promise<void>}\n     */\n    _processAnswerDebounced = Record.attr(null, {\n        compute() {\n            return debounce(\n                this._processAnswer,\n                this.script.isLivechatTourRunning ? 500 : Chatbot.MULTILINE_STEP_DEBOUNCE_DELAY\n            );\n        },\n    });\n\n    /**\n     * @param {import(\"models\").Message} message\n     */\n    async processAnswer(message) {\n        if (this.thread.notEq(message.thread) || !this.currentStep?.expectAnswer) {\n            return;\n        }\n        if (this.currentStep.type === \"free_input_multi\") {\n            await this._processAnswerDebounced(message);\n        }\n        await this._processAnswer(message);\n    }\n\n    async triggerNextStep() {\n        if (this.currentStep) {\n            await this._simulateTyping();\n        }\n        await this._goToNextStep();\n        if (!this.currentStep || this.currentStep.completed || !this.thread) {\n            return;\n        }\n        if (this.thread.isTransient) {\n            // Thread is not persisted thus messages do not exist on the server,\n            // create them now on the client side.\n            this.currentStep.message = this.store.Message.insert(\n                {\n                    id: this.store.getNextTemporaryId(),\n                    author: this.script.partner,\n                    body: this.currentStep.scriptStep.message,\n                    thread: this.thread,\n                },\n                { html: true }\n            );\n        }\n        this.thread.messages.add(this.currentStep.message);\n    }\n\n    get completed() {\n        return (\n            (this.currentStep?.isLast &&\n                (!this.currentStep.expectAnswer || this.currentStep?.completed)) ||\n            this.currentStep?.operatorFound\n        );\n    }\n\n    /**\n     * Go to the next step of the chatbot, fetch it if needed.\n     */\n    async _goToNextStep() {\n        if (!this.thread || this.currentStep?.isLast) {\n            return;\n        }\n        if (this.steps.at(-1)?.eq(this.currentStep)) {\n            const storeData = await rpc(\"/chatbot/step/trigger\", {\n                channel_id: this.thread.id,\n                chatbot_script_id: this.script.id,\n            });\n            if (!storeData) {\n                this.currentStep.isLast = true;\n                return;\n            }\n            const { ChatbotStep: steps } = this.store.insert(storeData, { html: true });\n            this.steps.push(steps[0]);\n        } else {\n            const nextStepIndex = this.steps.lastIndexOf(this.currentStep) + 1;\n            this.currentStep = this.steps[nextStepIndex];\n        }\n    }\n\n    /**\n     * Simulate the typing of the chatbot.\n     */\n    async _simulateTyping() {\n        this.isTyping = true;\n        await new Promise((res) =>\n            setTimeout(() => {\n                this.isTyping = false;\n                res();\n            }, Chatbot.MESSAGE_DELAY)\n        );\n    }\n\n    async _processAnswer(message) {\n        let stepCompleted = true;\n        if (this.currentStep.type === \"question_email\") {\n            stepCompleted = await this._processAnswerQuestionEmail();\n        } else if (this.currentStep.type === \"question_selection\") {\n            stepCompleted = await this._processAnswerQuestionSelection(message);\n        }\n        this.currentStep.completed = stepCompleted;\n    }\n\n    /**\n     * Process the user answer for a question selection step.\n     *\n     * @param {import(\"models\").Message} message Answer posted by the user.\n     * @returns {Promise<boolean>} Whether the script is ready to go to the next step.\n     */\n    async _processAnswerQuestionSelection(message) {\n        if (this.currentStep.selectedAnswer) {\n            return true;\n        }\n        const answer = this.currentStep.answers.find(({ name }) => message.body.includes(name));\n        this.currentStep.selectedAnswer = answer;\n        await rpc(\"/chatbot/answer/save\", {\n            channel_id: this.thread.id,\n            message_id: this.currentStep.message.id,\n            selected_answer_id: answer.id,\n        });\n        if (!answer.redirect_link) {\n            return true;\n        }\n        let isRedirecting = false;\n        if (answer.redirect_link && URL.canParse(answer.redirect_link, window.location.href)) {\n            const url = new URL(window.location.href);\n            const nextURL = new URL(answer.redirect_link, window.location.href);\n            isRedirecting = url.pathname !== nextURL.pathname || url.origin !== nextURL.origin;\n        }\n        const targetURL = new URL(answer.redirect_link, window.location.origin);\n        const redirectionAlreadyDone = targetURL.href === location.href;\n        if (!redirectionAlreadyDone) {\n            browser.location.assign(answer.redirect_link);\n        }\n        return redirectionAlreadyDone || !isRedirecting;\n    }\n\n    /**\n     * Process the user answer for a question email step.\n     *\n     * @returns {Promise<boolean>} Whether the script is ready to go to the next step.\n     */\n    async _processAnswerQuestionEmail() {\n        const { success, data } = await rpc(\"/chatbot/step/validate_email\", {\n            channel_id: this.thread.id,\n        });\n        const { Message: messages = [] } = this.store.insert(data, { html: true });\n        const [message] = messages;\n        if (message) {\n            this.thread.messages.add(message);\n        }\n        return success;\n    }\n\n    /**\n     * Restart the chatbot script.\n     */\n    restart() {\n        if (this.currentStep) {\n            this.currentStep.isLast = false;\n        }\n    }\n}\nChatbot.register();\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class ChatbotScript extends Record {\n    static id = \"id\";\n\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    name;\n    isLivechatTourRunning = false;\n    partner = Record.one(\"Persona\");\n    welcomeSteps = Record.many(\"chatbot.script.step\");\n}\nChatbotScript.register();\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class ChatbotScriptStepAnswer extends Record {\n    static id = \"id\";\n    static _name = \"chatbot.script.answer\";\n\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    label;\n    /** @type {string|false} */\n    redirect_link;\n}\nChatbotScriptStepAnswer.register();\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class ChatbotScriptStep extends Record {\n    static id = \"id\";\n    static _name = \"chatbot.script.step\";\n\n    /** @type {number} */\n    id;\n    /** @type {string} */\n    message;\n    /** @type {\"free_input_multi\"|\"free_input_single\"|\"question_email\"|\"question_phone\"|\"question_selection\"|\"text\"|\"forward_operator\"} */\n    type;\n    isLast = false;\n    answers = Record.many(\"chatbot.script.answer\");\n}\nChatbotScriptStep.register();\n", "import { SESSION_STATE } from \"@im_livechat/embed/common/livechat_service\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { EventBus, reactive } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nconst STEP_DELAY = 500;\nexport class ChatBotService {\n    /** @type {number} */\n    nextStepTimeout;\n\n    constructor(env, services) {\n        const self = reactive(this);\n        self.setup(env, services);\n        return self;\n    }\n\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {{\n     * \"im_livechat.livechat\": import(\"@im_livechat/embed/common/livechat_service\").LivechatService,\n     * \"mail.store\": import(\"@mail/core/common/store_service\").Store,\n     * }} services\n     */\n    setup(env, services) {\n        this.env = env;\n        this.bus = new EventBus();\n        this.livechatService = services[\"im_livechat.livechat\"];\n        this.store = services[\"mail.store\"];\n        services[\"mail.store\"].isReady.then(async () => {\n            if (this.chatbot) {\n                await this.livechatService.thread.isLoadedDeferred;\n                // wait for messages to be fully inserted\n                await new Promise(setTimeout);\n                this.start();\n            }\n        });\n        this.livechatService.onStateChange(SESSION_STATE.CREATED, () => {\n            if (this.chatbot) {\n                this.start();\n            }\n        });\n        this.livechatService.onStateChange(SESSION_STATE.NONE, () => this.stop());\n        this.bus.addEventListener(\"MESSAGE_POST\", async ({ detail: message }) => {\n            await this.chatbot?.processAnswer(message);\n            if (this.chatbot?.currentStep.completed) {\n                this._triggerNextStep();\n            }\n        });\n    }\n\n    /**\n     * Start the chatbot script.\n     */\n    async start() {\n        if (this.chatbot.thread.isLastMessageFromCustomer) {\n            await this.chatbot?.processAnswer(this.livechatService.thread.newestMessage);\n        }\n        if (!this.chatbot.currentStep?.expectAnswer || this.chatbot.currentStep?.completed) {\n            this._triggerNextStep();\n        }\n    }\n\n    /**\n     * Stop the chatbot script.\n     */\n    stop() {\n        clearTimeout(this.nextStepTimeout);\n    }\n\n    /**\n     * Restart the chatbot script if it was completed and post the\n     * restart message.\n     */\n    async restart() {\n        if (!this.chatbot?.completed) {\n            return;\n        }\n        const data = await rpc(\"/chatbot/restart\", {\n            channel_id: this.chatbot.thread.id,\n            chatbot_script_id: this.chatbot.script.id,\n        });\n        const { Message: messages = [] } = this.store.insert(data, { html: true });\n        if (!this.livechatService.thread) {\n            return;\n        }\n        const [message] = messages;\n        this.livechatService.thread.messages.add(message);\n        this.chatbot.restart();\n        this._triggerNextStep();\n    }\n\n    // =============================================================================\n    // SCRIPT PROCESSING\n    // =============================================================================\n\n    /**\n     * Trigger the next step of the script recursivly until the script is\n     * completed or the current step expects an answer from the user.\n     */\n    async _triggerNextStep() {\n        if (this.chatbot.completed) {\n            return;\n        }\n        await this.chatbot.triggerNextStep();\n        if (!this.chatbot?.currentStep) {\n            return;\n        }\n        if (this.chatbot.currentStep.expectAnswer || this.chatbot.currentStep.isLast) {\n            return;\n        }\n        this.nextStepTimeout = browser.setTimeout(async () => this._triggerNextStep(), STEP_DELAY);\n    }\n\n    // =============================================================================\n    // GETTERS\n    // =============================================================================\n\n    get canRestart() {\n        return this.chatbot?.completed && !this.chatbot.currentStep?.operatorFound;\n    }\n\n    get inputEnabled() {\n        if (!this.chatbot || this.chatbot.currentStep?.operatorFound) {\n            return true;\n        }\n        return (\n            !this.chatbot.currentStep?.completed &&\n            !this.isTyping &&\n            this.chatbot.currentStep?.expectAnswer &&\n            this.chatbot.currentStep?.answers.length === 0\n        );\n    }\n\n    get inputDisabledText() {\n        if (this.inputEnabled) {\n            return \"\";\n        }\n        if (this.chatbot.completed) {\n            return _t(\"Conversation ended...\");\n        }\n        if (\n            this.chatbot.currentStep?.type === \"question_selection\" &&\n            !this.chatbot.currentStep.completed\n        ) {\n            return _t(\"Select an option above\");\n        }\n        return _t(\"Say something\");\n    }\n\n    get chatbot() {\n        return this.livechatService.thread?.chatbot;\n    }\n}\n\nexport const chatBotService = {\n    dependencies: [\"im_livechat.livechat\", \"mail.store\"],\n    start(env, services) {\n        return new ChatBotService(env, services);\n    },\n};\nregistry.category(\"services\").add(\"im_livechat.chatbot\", chatBotService);\n", "import { AND, Record } from \"@mail/core/common/record\";\n\nexport class ChatbotStep extends Record {\n    static id = AND(\"scriptStep\", \"message\");\n\n    operatorFound = false;\n    scriptStep = Record.one(\"chatbot.script.step\");\n    message = Record.one(\"Message\", { inverse: \"chatbotStep\" });\n    answers = Record.many(\"chatbot.script.answer\", {\n        compute() {\n            return this.scriptStep?.answers;\n        },\n    });\n    selectedAnswer = Record.one(\"chatbot.script.answer\");\n    type = Record.attr(\"\", {\n        compute() {\n            return this.scriptStep?.type;\n        },\n    });\n    isLast = false;\n\n    get expectAnswer() {\n        return [\n            \"free_input_multi\",\n            \"free_input_single\",\n            \"question_selection\",\n            \"question_email\",\n            \"question_phone\",\n        ].includes(this.type);\n    }\n}\nChatbotStep.register();\n", "import { Component } from \"@odoo/owl\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\n\nexport class CloseConfirmation extends Component {\n    static template = \"im_livechat.CloseConfirmation\";\n    static props = [\"onCloseConfirmationDialog\", \"onClickLeaveConversation\"];\n\n    setup() {\n        useAutofocus({ refName: \"root\" });\n    }\n\n    onKeydown(ev) {\n        if (ev.key === \"Escape\") {\n            this.props.onCloseConfirmationDialog();\n        } else if (ev.key === \"Enter\") {\n            this.props.onClickLeaveConversation();\n        }\n    }\n}\n", "import { Composer } from \"@mail/core/common/composer\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { session } from \"@web/session\";\n\npatch(Composer.prototype, {\n    get placeholder() {\n        if (this.thread?.channel_type !== \"livechat\") {\n            return super.placeholder;\n        }\n        return session.livechatData.options.input_placeholder || _t(\"Say something...\");\n    },\n});\n", "import { threadActionsRegistry } from \"@mail/core/common/thread_actions\";\nimport { Thread } from \"@mail/core/common/thread_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    get hasMemberList() {\n        return false;\n    },\n    get hasAttachmentPanel() {\n        return this.channel_type !== \"livechat\" && super.hasAttachmentPanel;\n    },\n});\n\nconst allowedThreadActions = new Set([\"fold-chat-window\", \"close\", \"restart\", \"settings\"]);\nfor (const [actionName] of threadActionsRegistry.getEntries()) {\n    if (!allowedThreadActions.has(actionName)) {\n        threadActionsRegistry.remove(actionName);\n    }\n}\nthreadActionsRegistry.addEventListener(\"UPDATE\", ({ detail: { operation, key } }) => {\n    if (operation === \"add\" && !allowedThreadActions.has(key)) {\n        threadActionsRegistry.remove(key);\n    }\n});\n", "import { browser } from \"@web/core/browser/browser\";\n\nconst BASE_STORAGE_KEY = \"EXPIRABLE_STORAGE\";\nconst CLEAR_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds\n\nfunction cleanupExpirableStorage() {\n    const now = Date.now();\n    // Next line is for testing compatibility as for..in is not supported by\n    // the `MockStorage` class.\n    const keys = browser.localStorage.items?.keys() ?? Object.keys(browser.localStorage);\n    for (const key of keys) {\n        if (key.startsWith(BASE_STORAGE_KEY)) {\n            const item = JSON.parse(browser.localStorage.getItem(key));\n            if (item.expires && item.expires < now) {\n                browser.localStorage.removeItem(key);\n            }\n        }\n    }\n}\n\nexport const expirableStorage = {\n    /** @param {string} key */\n    getItem(key) {\n        cleanupExpirableStorage();\n        const item = browser.localStorage.getItem(`${BASE_STORAGE_KEY}_${key}`);\n        if (item) {\n            return JSON.parse(item).value;\n        }\n        return null;\n    },\n    /**\n     * @param {string} key\n     * @param {string} value\n     * @param {number} ttl Number of seconds after which the item should expire.\n     */\n    setItem(key, value, ttl) {\n        let expires;\n        if (ttl) {\n            expires = Date.now() + ttl * 1000;\n        }\n        browser.localStorage.setItem(\n            `${BASE_STORAGE_KEY}_${key}`,\n            JSON.stringify({ value, expires })\n        );\n    },\n    /** @param {string} key */\n    removeItem(key) {\n        browser.localStorage.removeItem(`${BASE_STORAGE_KEY}_${key}`);\n    },\n};\n\ncleanupExpirableStorage();\nsetInterval(cleanupExpirableStorage, CLEAR_INTERVAL);\n", "import { RATING } from \"@im_livechat/embed/common/livechat_service\";\nimport { TranscriptSender } from \"@im_livechat/embed/common/feedback_panel/transcript_sender\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { session } from \"@web/session\";\nimport { url } from \"@web/core/utils/urls\";\nimport { rpc } from \"@web/core/network/rpc\";\n\n/**\n * @typedef {Object} Props\n * @property {Function} [onClickClose]\n * @property {import(\"models\").Thread}\n * @extends {Component<Props, Env>}\n */\nexport class FeedbackPanel extends Component {\n    static template = \"im_livechat.FeedbackPanel\";\n    static props = [\"onClickClose?\", \"thread\"];\n    static components = { TranscriptSender };\n\n    STEP = Object.freeze({\n        RATING: \"rating\",\n        THANKS: \"thanks\",\n    });\n    RATING = RATING;\n\n    setup() {\n        this.session = session;\n        this.livechatService = useService(\"im_livechat.livechat\");\n        this.state = useState({\n            step: this.STEP.RATING,\n            rating: null,\n            feedback: \"\",\n        });\n        this.url = url;\n    }\n\n    /**\n     * @param {number} rating\n     */\n    select(rating) {\n        this.state.rating = rating;\n    }\n\n    onClickSendFeedback() {\n        rpc(\"/im_livechat/feedback\", {\n            reason: this.state.feedback,\n            rate: this.state.rating,\n            channel_id: this.props.thread.id,\n        });\n        this.state.step = this.STEP.THANKS;\n    }\n}\n", "import { isValidEmail } from \"@im_livechat/embed/common/misc\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Thread}\n * @extends {Component<Props, Env>}\n */\nexport class TranscriptSender extends Component {\n    static template = \"im_livechat.TranscriptSender\";\n    static props = [\"thread\"];\n\n    STATUS = Object.freeze({\n        IDLE: \"idle\",\n        SENDING: \"sending\",\n        SENT: \"sent\",\n        FAILED: \"failed\",\n    });\n\n    setup() {\n        this.isValidEmail = isValidEmail;\n        this.livechatService = useService(\"im_livechat.livechat\");\n        this.state = useState({\n            email: \"\",\n            status: this.STATUS.IDLE,\n        });\n    }\n\n    async onClickSend() {\n        this.state.status = this.STATUS.SENDING;\n        try {\n            await rpc(\"/im_livechat/email_livechat_transcript\", {\n                channel_id: this.props.thread.id,\n                email: this.state.email,\n            });\n            this.state.status = this.STATUS.SENT;\n        } catch {\n            this.state.status = this.STATUS.FAILED;\n        }\n    }\n}\n", "import { expirableStorage } from \"@im_livechat/embed/common/expirable_storage\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\n\nexport class HistoryService {\n    static HISTORY_STORAGE_KEY = \"im_livechat_history\";\n    static HISTORY_LIMIT = 15;\n\n    constructor(env, services) {\n        /** @type {ReturnType<typeof import(\"@bus/services/bus_service\").busService.start>} */\n        this.busService = services.bus_service;\n        /** @type {import(\"@im_livechat/embed/common/livechat_service\").LivechatService} */\n        this.livechatService = services[\"im_livechat.livechat\"];\n    }\n\n    setup() {\n        this.updateHistory();\n        this.busService.subscribe(\"im_livechat.history_command\", (payload) => {\n            if (payload.id !== this.livechatService.thread?.id) {\n                return;\n            }\n            const data = expirableStorage.getItem(HistoryService.HISTORY_STORAGE_KEY);\n            const history = data ? JSON.parse(data) : [];\n            rpc(\"/im_livechat/history\", {\n                pid: this.livechatService.thread.operator.id,\n                channel_id: this.livechatService.thread.id,\n                page_history: history,\n            });\n        });\n    }\n\n    updateHistory() {\n        const page = browser.location.href.replace(/^.*\\/\\/[^/]+/, \"\");\n        const pageHistory = expirableStorage.getItem(HistoryService.HISTORY_STORAGE_KEY);\n        const urlHistory = pageHistory ? JSON.parse(pageHistory) : [];\n        if (!urlHistory.includes(page)) {\n            urlHistory.push(page);\n            if (urlHistory.length > HistoryService.HISTORY_LIMIT) {\n                urlHistory.shift();\n            }\n            expirableStorage.setItem(\n                HistoryService.HISTORY_STORAGE_KEY,\n                JSON.stringify(urlHistory),\n                60 * 60 * 24 // kept for 1 day\n            );\n        }\n    }\n}\n\nexport const historyService = {\n    dependencies: [\"im_livechat.livechat\", \"bus_service\"],\n    start(env, services) {\n        const history = new HistoryService(env, services);\n        history.setup();\n    },\n};\n\nregistry.category(\"services\").add(\"im_livechat.history_service\", historyService);\n", "import { SESSION_STATE } from \"@im_livechat/embed/common/livechat_service\";\nimport { Component, useExternalListener, useRef, useState } from \"@odoo/owl\";\n\nimport { useMovable } from \"@mail/utils/common/hooks\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { debounce } from \"@web/core/utils/timing\";\n\nconst LIVECHAT_BUTTON_SIZE = 56;\n\nexport class LivechatButton extends Component {\n    static template = \"im_livechat.LivechatButton\";\n    static props = {};\n    static DEBOUNCE_DELAY = 500;\n\n    setup() {\n        this.store = useState(useService(\"mail.store\"));\n        /** @type {import('@im_livechat/embed/common/livechat_service').LivechatService} */\n        this.livechatService = useState(useService(\"im_livechat.livechat\"));\n        this.onClick = debounce(this.onClick.bind(this), LivechatButton.DEBOUNCE_DELAY, {\n            leading: true,\n        });\n        this.ref = useRef(\"button\");\n        this.size = LIVECHAT_BUTTON_SIZE;\n        this.position = useState({\n            left: `calc(97% - ${LIVECHAT_BUTTON_SIZE}px)`,\n            top: `calc(97% - ${LIVECHAT_BUTTON_SIZE}px)`,\n        });\n        this.state = useState({\n            animateNotification: !(\n                this.livechatService.thread || this.livechatService.shouldRestoreSession\n            ),\n            hasAlreadyMovedOnce: false,\n        });\n        useMovable({\n            cursor: \"grabbing\",\n            ref: this.ref,\n            elements: \".o-livechat-LivechatButton\",\n            onDrop: ({ top, left }) => {\n                this.state.hasAlreadyMovedOnce = true;\n                this.position.left = `${left}px`;\n                this.position.top = `${top}px`;\n            },\n        });\n        useExternalListener(document.body, \"scroll\", this._onScroll, { capture: true });\n    }\n\n    _onScroll(ev) {\n        if (!this.ref.el || this.state.hasAlreadyMovedOnce) {\n            return;\n        }\n        const container = ev.target;\n        this.position.top =\n            container.scrollHeight - container.scrollTop === container.clientHeight\n                ? `calc(93% - ${LIVECHAT_BUTTON_SIZE}px)`\n                : `calc(97% - ${LIVECHAT_BUTTON_SIZE}px)`;\n    }\n\n    onClick() {\n        this.state.animateNotification = false;\n        this.livechatService.open();\n    }\n\n    get isShown() {\n        return (\n            this.livechatService.initialized &&\n            this.livechatService.available &&\n            this.livechatService.state === SESSION_STATE.NONE &&\n            Object.keys(this.store.ChatWindow.records).length === 0\n        );\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\nexport const livechatInitializedService = {\n    start() {\n        return {\n            ready: new Deferred(),\n        };\n    },\n};\nregistry.category(\"services\").add(\"im_livechat.initialized\", livechatInitializedService);\n", "import { Record } from \"@mail/core/common/record\";\n\nexport class LivechatRule extends Record {\n    /** @type {\"auto_popup\"|\"display_button_and_text\"|undefined} */\n    action;\n    /** @type {number} */\n    autoPopupTimer;\n    /** @type {string} */\n    regexURL;\n    chatbotScript = Record.one(\"ChatbotScript\");\n}\nLivechatRule.register();\n", "import { expirableStorage } from \"@im_livechat/embed/common/expirable_storage\";\n\nimport { reactive } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { user } from \"@web/core/user\";\n\n/**\n * @typedef LivechatRule\n * @property {\"auto_popup\"|\"display_button_and_text\"|undefined} [action]\n * @property {number?} [auto_popup_timer]\n * @property {import(\"@im_livechat/embed/common/chatbot/chatbot_model\").IChatbot} [chatbot]\n */\n\nexport const RATING = Object.freeze({\n    GOOD: 5,\n    OK: 3,\n    BAD: 1,\n});\n\nexport const SESSION_STATE = Object.freeze({\n    NONE: \"NONE\",\n    CREATED: \"CREATED\",\n    PERSISTED: \"PERSISTED\",\n});\n\nexport const ODOO_VERSION_KEY = `${location.origin.replace(\n    /:\\/{0,2}/g,\n    \"_\"\n)}_im_livechat.odoo_version`;\n\nconst OPERATOR_STORAGE_KEY = \"im_livechat_previous_operator\";\nconst GUEST_TOKEN_STORAGE_KEY = \"im_livechat_guest_token\";\nconst SAVED_STATE_STORAGE_KEY = \"im_livechat.saved_state\";\nconst LIVECHAT_UUID_COOKIE = \"im_livechat_uuid\";\n\nexport function getGuestToken() {\n    return expirableStorage.getItem(GUEST_TOKEN_STORAGE_KEY);\n}\n\nexport class LivechatService {\n    /** @type {keyof typeof SESSION_STATE} */\n    state = SESSION_STATE.NONE;\n    /** @type {LivechatRule} */\n    rule;\n    initialized = false;\n    available = session.livechatData?.isAvailable;\n    /** @type {import(\"models\").Thread} */\n    thread;\n    _onStateChangeCallbacks = {\n        [SESSION_STATE.CREATED]: [],\n        [SESSION_STATE.PERSISTED]: [],\n        [SESSION_STATE.NONE]: [],\n    };\n\n    constructor(env, services) {\n        this.setup(env, services);\n    }\n\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {{\n     * bus_service: ReturnType<typeof import(\"@bus/services/bus_service\").busService.start>,\n     * \"mail.store\": import(\"@mail/core/common/store_service\").Store\n     * }} services\n     */\n    setup(env, services) {\n        this.env = env;\n        this.busService = services.bus_service;\n        this.notificationService = services.notification;\n        this.store = services[\"mail.store\"];\n    }\n\n    async initialize() {\n        const data =\n            this.options?.init ??\n            (await rpc(\"/im_livechat/init\", {\n                channel_id: this.options.channel_id,\n            }));\n        this.available = data.available_for_me;\n        this.rule = this.store.LivechatRule.insert(data.rule);\n        this.store.insert(data.storeData);\n        if (this.options?.force_thread) {\n            this.state = SESSION_STATE.PERSISTED;\n            this.thread = this.store.Thread.insert(this.options.force_thread);\n            this._saveLivechatState();\n        }\n        if (this.savedState) {\n            this.state = this.savedState.persisted\n                ? SESSION_STATE.PERSISTED\n                : SESSION_STATE.CREATED;\n        }\n        if (this.state === SESSION_STATE.PERSISTED) {\n            await this.busService.addChannel(`mail.guest_${this.guestToken}`);\n        }\n        this.initialized = true;\n        this.env.services[\"im_livechat.initialized\"].ready.resolve();\n    }\n\n    /**\n     * Open a new live chat thread.\n     *\n     * @returns {Promise<import(\"models\").Thread|undefined>}\n     */\n    async open() {\n        await this._createThread({ persist: false });\n        this.thread?.openChatWindow();\n    }\n\n    /**\n     * Persist the livechat thread if it is not done yet and swap it with the\n     * temporary thread.\n     *\n     * @returns {Promise<import(\"models\").Thread|undefined>}\n     */\n    async persist() {\n        if (this.state === SESSION_STATE.PERSISTED) {\n            return this.thread;\n        }\n        const temporaryThread = this.thread;\n        await this._createThread({ persist: true });\n        if (temporaryThread) {\n            const chatWindow = this.store.ChatWindow.get({ thread: temporaryThread });\n            temporaryThread.delete();\n            await chatWindow.close();\n        }\n        if (!this.thread) {\n            return;\n        }\n        this.store.chatHub.opened.add({ thread: this.thread }).autofocus++;\n        await this.busService.addChannel(`mail.guest_${this.guestToken}`);\n        await this.env.services[\"mail.store\"].initialize();\n        return this.thread;\n    }\n\n    /**\n     * @param {object} param0\n     * @param {boolean} param0.notifyServer Whether to call the\n     * `visitor_leave_session` route. Note that this route will never be called\n     * if the session was not persisted.\n     */\n    async leave({ notifyServer = true } = {}) {\n        try {\n            if (this.thread && this.state === SESSION_STATE.PERSISTED && notifyServer) {\n                await rpc(\"/im_livechat/visitor_leave_session\", { channel_id: this.thread.id });\n            }\n        } finally {\n            this.thread = undefined;\n            expirableStorage.removeItem(SAVED_STATE_STORAGE_KEY);\n            cookie.delete(LIVECHAT_UUID_COOKIE);\n            this.state = SESSION_STATE.NONE;\n            await Promise.all(this._onStateChangeCallbacks[SESSION_STATE.NONE].map((fn) => fn()));\n        }\n    }\n\n    /**\n     * Add a callback to be executed when the livechat service state changes.\n     *\n     * @param {keyof typeof SESSION_STATE} state\n     * @param {Function} callback\n     */\n    onStateChange(state, callback) {\n        this._onStateChangeCallbacks[state].push(callback);\n    }\n\n    /**\n     * Save the current live chat state. Only save the strict minimum if the\n     * thread is persisted.\n     *\n     * @param {Object} [saveData]\n     */\n    _saveLivechatState(saveData) {\n        const { guest_token } = this.store;\n        if (guest_token) {\n            expirableStorage.setItem(GUEST_TOKEN_STORAGE_KEY, guest_token);\n        }\n        const ONE_DAY_TTL = 60 * 60 * 24;\n        if (this.thread.uuid) {\n            cookie.set(LIVECHAT_UUID_COOKIE, this.thread.uuid, ONE_DAY_TTL);\n        }\n        const persisted = this.state === SESSION_STATE.PERSISTED;\n        expirableStorage.setItem(\n            SAVED_STATE_STORAGE_KEY,\n            JSON.stringify({\n                livechatUserId: this.savedState?.livechatUserId ?? user.userId,\n                persisted,\n                store: persisted ? { \"discuss.channel\": [{ id: this.thread.id }] } : saveData,\n            }),\n            ONE_DAY_TTL\n        );\n        if (this.thread.operator) {\n            expirableStorage.setItem(\n                OPERATOR_STORAGE_KEY,\n                this.thread.operator.id,\n                ONE_DAY_TTL * 7\n            );\n        }\n    }\n\n    /**\n     * @param {object} param0\n     * @param {boolean} [param0.persist=false]\n     * @returns {Promise<import(\"models\").Thread>}\n     */\n    async _createThread({ persist = false }) {\n        const data = await rpc(\n            \"/im_livechat/get_session\",\n            {\n                channel_id: this.options.channel_id,\n                anonymous_name: this.options.default_username ?? _t(\"Visitor\"),\n                chatbot_script_id: this.savedState\n                    ? this.thread.chatbot?.script.id\n                    : this.rule.chatbotScript?.id,\n                previous_operator_id: expirableStorage.getItem(OPERATOR_STORAGE_KEY),\n                temporary_id: this.thread?.id,\n                persisted: persist,\n            },\n            { shadow: true }\n        );\n        // clean copy of data for saving in storage, because store insert will add cyclic references\n        const saveData = JSON.parse(JSON.stringify(data));\n        const { Thread = [] } = this.store.insert(data);\n        this.thread = Thread[0];\n        if (!this.thread?.operator) {\n            this.notificationService.add(_t(\"No available collaborator, please try again later.\"));\n            this.leave({ notifyServer: false });\n            return;\n        }\n        this.state = persist ? SESSION_STATE.PERSISTED : SESSION_STATE.CREATED;\n        this._saveLivechatState(saveData);\n        await Promise.all(this._onStateChangeCallbacks[this.state].map((fn) => fn()));\n    }\n\n    get options() {\n        return session.livechatData?.options ?? {};\n    }\n\n    get savedState() {\n        return JSON.parse(expirableStorage.getItem(SAVED_STATE_STORAGE_KEY) ?? false);\n    }\n\n    /**\n     * @returns {string|undefined}\n     */\n    get guestToken() {\n        return getGuestToken();\n    }\n}\n\nexport const livechatService = {\n    dependencies: [\"bus_service\", \"im_livechat.initialized\", \"mail.store\", \"notification\"],\n    start(env, services) {\n        const livechat = reactive(new LivechatService(env, services));\n        (async () => {\n            // Live chat state should be deleted if it is linked to another user\n            // (log in/out after chat start).\n            if ((livechat.savedState?.livechatUserId || false) !== (user.userId || false)) {\n                await livechat.leave({ notifyServer: false });\n            }\n            if (livechat.available) {\n                livechat.initialize();\n            }\n        })();\n        return livechat;\n    },\n};\nregistry.category(\"services\").add(\"im_livechat.livechat\", livechatService);\n", "import { Message } from \"@mail/core/common/message_model\";\nimport { Record } from \"@mail/core/common/record\";\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { SESSION_STATE } from \"./livechat_service\";\n\npatch(Message.prototype, {\n    setup() {\n        super.setup();\n        this.chatbotStep = Record.one(\"ChatbotStep\", { inverse: \"message\" });\n    },\n    canAddReaction(thread) {\n        return (\n            super.canAddReaction(thread) &&\n            (thread?.channel_type !== \"livechat\" ||\n                this.store.env.services[\"im_livechat.livechat\"].state === SESSION_STATE.PERSISTED)\n        );\n    },\n    canReplyTo(thread) {\n        return (\n            super.canReplyTo(thread) &&\n            (thread?.channel_type !== \"livechat\" ||\n                this.store.env.services[\"im_livechat.chatbot\"].inputEnabled)\n        );\n    },\n});\n", "import { Message } from \"@mail/core/common/message\";\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { url } from \"@web/core/utils/urls\";\n\nMessage.props.push(\"isTypingMessage?\");\n\npatch(Message.prototype, {\n    setup() {\n        super.setup();\n        this.url = url;\n    },\n\n    get quickActionCount() {\n        return this.props.thread?.channel_type === \"livechat\" ? 3 : super.quickActionCount;\n    },\n\n    /**\n     * @param {import(\"@im_livechat/embed/common/chatbot/chatbot_step_model\").StepAnswer} answer\n     */\n    answerChatbot(answer) {\n        return this.props.message.thread.post(answer.name);\n    },\n});\n", "export function isValidEmail(val) {\n    // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript\n    const re =\n        /^(([^<>()[\\].,;:\\s@\"]+(\\.[^<>()[\\].,;:\\s@\"]+)*)|(\".+\"))@(([^<>()[\\].,;:\\s@\"]+\\.)+[^<>()[\\].,;:\\s@\"]{2,})$/i;\n    return re.test(val);\n}\n", "import { SESSION_STATE } from \"@im_livechat/embed/common/livechat_service\";\n\nimport { Store, storeService } from \"@mail/core/common/store_service\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\nstoreService.dependencies.push(\"im_livechat.initialized\");\n\npatch(Store.prototype, {\n    async initialize() {\n        const livechatInitialized = this.env.services[\"im_livechat.initialized\"];\n        await livechatInitialized.ready;\n        const livechatService = this.env.services[\"im_livechat.livechat\"];\n        if (livechatService.state === SESSION_STATE.PERSISTED) {\n            await super.initialize();\n            livechatService.thread ??= this.store.Thread.get({\n                id: livechatService.savedState?.store[\"discuss.channel\"][0].id,\n                model: \"discuss.channel\",\n            });\n            if (!livechatService.thread) {\n                livechatService.leave({ notifyServer: false });\n            }\n            return;\n        }\n        if (livechatService.savedState?.store) {\n            const { Thread = [] } = this.store.insert(livechatService.savedState.store);\n            livechatService.thread = Thread[0];\n        }\n        this.isReady.resolve();\n    },\n    get initMessagingParams() {\n        const params = super.initMessagingParams;\n        params.init_messaging.channel_types = [\"livechat\"];\n        return params;\n    },\n});\n", "import { SESSION_STATE } from \"@im_livechat/embed/common/livechat_service\";\n\nimport { threadActionsRegistry } from \"@mail/core/common/thread_actions\";\nimport \"@mail/discuss/call/common/thread_actions\";\nimport { useComponent } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\n\nthreadActionsRegistry.add(\"restart\", {\n    condition(component) {\n        return component.chatbotService.canRestart;\n    },\n    icon: \"fa fa-fw fa-refresh\",\n    name: _t(\"Restart Conversation\"),\n    open(component) {\n        component.chatbotService.restart();\n        component.props.chatWindow.open();\n    },\n    sequence: 99,\n    sequenceQuick: 15,\n});\n\nconst callSettingsAction = threadActionsRegistry.get(\"settings\");\npatch(callSettingsAction, {\n    condition(component) {\n        if (component.thread?.channel_type !== \"livechat\") {\n            return super.condition(...arguments);\n        }\n        return (\n            component.livechatService.state === SESSION_STATE.PERSISTED &&\n            component.rtcService.state.channel?.eq(component.thread)\n        );\n    },\n    setup() {\n        super.setup(...arguments);\n        const component = useComponent();\n        component.livechatService = useService(\"im_livechat.livechat\");\n        component.rtcService = useService(\"discuss.rtc\");\n    },\n});\n", "import { Record } from \"@mail/core/common/record\";\nimport { Thread } from \"@mail/core/common/thread_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { SESSION_STATE } from \"./livechat_service\";\n\npatch(Thread.prototype, {\n    setup() {\n        super.setup();\n        this.chatbotTypingMessage = Record.one(\"Message\", {\n            compute() {\n                if (this.chatbot) {\n                    return { id: -0.1 - this.id, thread: this, author: this.operator };\n                }\n            },\n        });\n        this.livechatWelcomeMessage = Record.one(\"Message\", {\n            compute() {\n                if (this.hasWelcomeMessage) {\n                    const livechatService = this.store.env.services[\"im_livechat.livechat\"];\n                    return {\n                        id: -0.2 - this.id,\n                        body: livechatService.options.default_message,\n                        thread: this,\n                        author: this.operator,\n                    };\n                }\n            },\n        });\n        this.chatbot = Record.one(\"Chatbot\");\n        this.requested_by_operator = false;\n    },\n\n    get isLastMessageFromCustomer() {\n        if (this.channel_type !== \"livechat\") {\n            return super.isLastMessageFromCustomer;\n        }\n        return this.newestMessage?.isSelfAuthored;\n    },\n\n    get membersThatCanSeen() {\n        return super.membersThatCanSeen.filter((member) => !member.is_bot);\n    },\n\n    get avatarUrl() {\n        if (this.channel_type === \"livechat\") {\n            return this.operator.avatarUrl;\n        }\n        return super.avatarUrl;\n    },\n    get displayName() {\n        if (this.channel_type === \"livechat\" && this.operator) {\n            return this.operator.user_livechat_username || this.operator.name;\n        }\n        return super.displayName;\n    },\n    get hasWelcomeMessage() {\n        return this.channel_type === \"livechat\" && !this.chatbot && !this.requested_by_operator;\n    },\n    /** @returns {Promise<import(\"models\").Message} */\n    async post() {\n        if (\n            this.channel_type === \"livechat\" &&\n            this.store.env.services[\"im_livechat.livechat\"].state !== SESSION_STATE.PERSISTED\n        ) {\n            const thread = await this.store.env.services[\"im_livechat.livechat\"].persist();\n            if (!thread) {\n                return;\n            }\n            return thread.post(...arguments);\n        }\n        const message = await super.post(...arguments);\n        this.store.env.services[\"im_livechat.chatbot\"].bus.trigger(\"MESSAGE_POST\", message);\n        return message;\n    },\n\n    get showUnreadBanner() {\n        if (this.chatbot && !this.chatbot.currentStep?.operatorFound) {\n            return false;\n        }\n        return super.showUnreadBanner;\n    },\n});\n", "import { Thread } from \"@mail/core/common/thread\";\n\nimport { useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    setup() {\n        super.setup();\n        this.chatbotService = useState(useService(\"im_livechat.chatbot\"));\n    },\n});\n", "import { makeRoot, makeShadow } from \"@im_livechat/embed/common/boot_helpers\";\nimport { LivechatRoot } from \"@im_livechat/embed/frontend/livechat_root\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { App } from \"@odoo/owl\";\n\nimport { getTemplate } from \"@web/core/templates\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\n\nregistry.category(\"main_components\").remove(\"mail.ChatHub\");\n\nexport const livechatBootService = {\n    dependencies: [\"mail.store\"],\n\n    /**\n     * To be overriden in tests.\n     */\n    getTarget() {\n        return document.body;\n    },\n\n    start(env) {\n        if (!session.livechatData?.isAvailable) {\n            return;\n        }\n        const target = this.getTarget();\n        const root = makeRoot(target);\n        makeShadow(root).then((shadow) => {\n            new App(LivechatRoot, {\n                env,\n                getTemplate,\n                translatableAttributes: [\"data-tooltip\"],\n                translateFn: _t,\n                dev: env.debug,\n            }).mount(shadow);\n        });\n    },\n};\nregistry.category(\"services\").add(\"im_livechat.boot\", livechatBootService);\n", "import { LivechatButton } from \"@im_livechat/embed/common/livechat_button\";\n\nimport { ChatHub } from \"@mail/core/common/chat_hub\";\n\nimport { Component, useSubEnv, xml } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n// overlay inside shadow so that the styles are dicted by the shadow dom\nimport { OverlayContainer } from \"@web/core/overlay/overlay_container\";\n\nexport class LivechatRoot extends Component {\n    static template = xml`\n        <ChatHub/>\n        <LivechatButton/>\n        <OverlayContainer overlays=\"overlayService.overlays\"/>\n    `;\n    static components = { ChatHub, LivechatButton, OverlayContainer };\n    static props = {};\n\n    setup() {\n        useSubEnv({ embedLivechat: true });\n        this.overlayService = useService(\"overlay\");\n    }\n}\n", "/* @odoo-module */\nimport { parseEmail } from \"@mail/utils/common/format\";\n\n/**\n * splits the string and find all the invalid emails from it.\n *\n * @param {string}\n * @return {object}\n */\nfunction findInvalidEmailFromText(emailStr){\n    const emailList = emailStr.split('\\n');\n    const invalidEmails = emailList.filter(email => email !== '' && !parseEmail(email.trim())[1]);\n    const emailInfo = {\n        'invalidEmails': invalidEmails,\n        'emailList': emailList,\n    }\n    return emailInfo\n}\n\nexport {\n    findInvalidEmailFromText\n};\n", "/** @odoo-module **/\n\nimport { renderToElement } from \"@web/core/utils/render\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { rpc } from \"@web/core/network/rpc\";\n\npublicWidget.registry.appointmentTypeSelect = publicWidget.Widget.extend({\n    selector: '.o_appointment_choice',\n    events: {\n        'change select[id=\"appointment_type_id\"]': '_onAppointmentTypeChange',\n        'click .o_appointment_select_button': '_onAppointmentTypeSelected',\n    },\n\n    /**\n     * @constructor\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n        // Check if we cannot replace this by a async handler once the related\n        // task is merged in master\n        this._onAppointmentTypeChange = debounce(this._onAppointmentTypeChange, 250);\n    },\n\n    /**\n     * @override\n     */\n    start: function () {\n        return this._super(...arguments).then(() => {\n            // Load an image when no appointment types are found\n            this.el.querySelector(\".o_appointment_svg i\")?.replaceWith(renderToElement('Appointment.appointment_svg', {}));\n            this.el\n                .querySelectorAll(\".o_appointment_not_found div\")\n                .forEach((el) => el.classList.remove(\"d-none\"));\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * On appointment type change: adapt appointment intro text and available\n     * users. (if option enabled)\n     *\n     * @override\n     * @param {Event} ev\n     */\n    _onAppointmentTypeChange: function (ev) {\n        var self = this;\n        const appointmentTypeID = ev.target.value;\n        const filterAppointmentTypeIds = this.el.querySelector(\n            \"input[name='filter_appointment_type_ids']\"\n        ).value;\n        const filterUserIds = this.el.querySelector(\"input[name='filter_staff_user_ids']\").value;\n        const filterResourceIds = this.el.querySelector(\"input[name='filter_resource_ids']\").value;\n        const inviteToken = this.el.querySelector(\"input[name='invite_token']\").value;\n\n        rpc(`/appointment/${appointmentTypeID}/get_message_intro`, {\n            invite_token: inviteToken,\n            filter_appointment_type_ids: filterAppointmentTypeIds,\n            filter_staff_user_ids: filterUserIds,\n            filter_resource_ids: filterResourceIds,\n        }).then(function (message_intro) {\n            self.el.querySelector(\".o_appointment_intro\")?.replaceChildren(message_intro);\n        });\n    },\n\n    _onAppointmentTypeSelected: function (ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        const optionSelected = this.el.querySelector('select').selectedOptions[0];\n        window.location = optionSelected.dataset.appointmentUrl;\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { renderToElement, renderToFragment } from \"@web/core/utils/render\";\nimport { serializeDateTime, deserializeDateTime } from \"@web/core/l10n/dates\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nconst { DateTime } = luxon;\n\npublicWidget.registry.appointmentSlotSelect = publicWidget.Widget.extend({\n    selector: '.o_appointment_info',\n    events: {\n        'change select[name=\"timezone\"]': '_onRefresh',\n        'change select[id=\"selectAppointmentResource\"]': '_onRefresh',\n        'change select[id=\"selectStaffUser\"]': '_onRefresh',\n        'change select[id=\"resourceCapacity\"]': '_onRefresh',\n        'click .o_js_calendar_navigate': '_onCalendarNavigate',\n        'click .o_slot_button': '_onClickDaySlot',\n        'click .o_slot_hours': '_onClickHoursSlot',\n        'click button[name=\"submitSlotInfoSelected\"]': '_onClickConfirmSlot',\n        'click .o_appointment_show_calendar': '_onClickShowCalendar',\n    },\n\n    /**\n     * @override\n     */\n    start: function () {\n        return this._super(...arguments).then(async () => {\n            await this.initSlots();\n            this._removeLoadingSpinner();\n            this.firstEl?.click();\n        });\n    },\n\n    /**\n     * Initializes variables and design\n     * - slotsListEl: the block containing the availabilities\n     * - resourceSelectionEl: resources or users selection for time_resource mode\n     * - firstEl: the first day containing a slot\n     */\n    initSlots: async function () {\n        this.slotsListEl = this.el.querySelector(\"#slotsList\");\n        this.resourceSelectionEl = this.el.querySelector(\"#resourceSelection\");\n        this.firstEl = this.el.querySelector(\".o_slot_button\");\n        await this._updateSlotAvailability();\n    },\n\n    /**\n     * Finds the first day with an available slot, replaces the currently shown month and\n     * click on the first date where a slot is available.\n     */\n    selectFirstAvailableMonth: function () {\n        const firstMonthEl = this.firstEl.closest(\".o_appointment_month\");\n        const currentMonthEl = document.querySelector(\".o_appointment_month:not(.d-none)\");\n        currentMonthEl.classList.add(\"d-none\");\n        currentMonthEl\n            .querySelectorAll(\"table\")\n            .forEach((table) => table.classList.remove(\"d-none\"));\n        currentMonthEl.querySelector(\".o_appointment_no_slot_month_helper\").remove();\n        firstMonthEl.classList.remove(\"d-none\");\n        this.slotsListEl.replaceChildren();\n        this.firstEl.click();\n    },\n\n    /**\n     * Replaces the content of the calendar month with the no month helper.\n     * Renders and appends its template to the element given as argument.\n     * - monthEl: the month div to which we append the helper.\n     */\n    _renderNoAvailabilityForMonth: function (monthEl) {\n        const firstAvailabilityDate = this.firstEl.getAttribute(\"id\");\n        const staffUserEl = this.el.querySelector(\"#slots_form select[name='staff_user_id']\");\n        const staffUserNameSelectedOption = staffUserEl?.options[staffUserEl.selectedIndex];\n        const staffUserName = staffUserNameSelectedOption?.textContent;\n        monthEl.querySelectorAll(\"table\").forEach((tableEl) => tableEl.classList.add(\"d-none\"));\n        monthEl.append(\n            renderToElement(\"Appointment.appointment_info_no_slot_month\", {\n                firstAvailabilityDate: DateTime.fromISO(firstAvailabilityDate).toFormat(\"cccc dd MMMM yyyy\"),\n                staffUserName: staffUserName,\n            })\n        );\n        monthEl\n            .querySelector(\"#next_available_slot\")\n            .addEventListener(\"click\", () => this.selectFirstAvailableMonth());\n    },\n\n    /**\n     * Checks whether any slot is available in the calendar.\n     * If there isn't, adds an explicative message in the slot list, and hides the appointment details,\n     * and make design width adjustment to have the helper message centered to the whole width.\n     * In case, there is no slots based on capacity chosen then the details and calendar are not hidden.\n     * If the appointment is missconfigured (missing user or missing availabilities),\n     * display an explicative message. The calendar is then not displayed.\n     * If there is an upcoming appointment booked, display a information before the the calendar\n     *\n     */\n    _updateSlotAvailability: async function () {\n        if (!this.firstEl) { // No slot available\n            if (!this.el.querySelector(\"select[name='resourceCapacity']\")) {\n                this.el\n                    .querySelectorAll(\"#slots_availabilities\")\n                    .forEach((slotEl) => slotEl.replaceChildren());\n                this.el.querySelector(\".o_appointment_timezone_selection\")?.classList.add(\"d-none\");\n\n                const staffUserEl = this.el.querySelector(\n                    \"#slots_form select[name='staff_user_id']\"\n                );\n                const staffUserNameSelectedOption = staffUserEl?.options[staffUserEl.selectedIndex];\n                const staffUserName = staffUserNameSelectedOption?.textContent;\n                const hideSelectDropdown = !!this.el.querySelector(\n                    \"input[name='hide_select_dropdown']\"\n                ).value;\n                const active = this.el.querySelector(\"input[name='active']\").value;\n                this.el.querySelector(\".o_appointment_no_slot_overall_helper\").replaceChildren(\n                    renderToElement(\"Appointment.appointment_info_no_slot\", {\n                        active: active,\n                        appointmentsCount: parseInt(\n                            this.el.querySelector(\"#slotsList\").dataset.appointmentsCount\n                        ),\n                        staffUserName: hideSelectDropdown ? staffUserName : false,\n                    })\n                );\n            } else {\n                this.el\n                    .querySelector(\".o_appointment_no_capacity\")\n                    ?.replaceChildren(renderToElement(\"Appointment.appointment_info_no_capacity\"));\n            }\n        } else {\n            this.el.querySelector(\".o_appointment_timezone_selection\")?.classList.remove(\"d-none\");\n            this.el.querySelector(\".o_appointment_no_capacity\")?.replaceChildren();\n        }\n        this.el.querySelector(\".o_appointment_missing_configuration\")?.classList.remove(\"d-none\");\n        // Check upcoming appointments\n        const allAppointmentsToken = JSON.parse(localStorage.getItem('appointment.upcoming_events_access_token')) || [];\n        const ignoreUpcomingEventUntil = localStorage.getItem('appointment.upcoming_events_ignore_until');\n        if (\n            !this.el.querySelector('.o_appointment_cancelled') &&\n            (!ignoreUpcomingEventUntil || deserializeDateTime(ignoreUpcomingEventUntil) < DateTime.utc()) &&\n            (allAppointmentsToken.length !== 0 || user.userId !== false)\n        ) {\n            const upcomingAppointmentData = await rpc(\"/appointment/get_upcoming_appointments\", {\n                calendar_event_access_tokens: allAppointmentsToken,\n            });\n            if (upcomingAppointmentData) {\n                this.el.querySelector('div.o_appointment_calendar').classList.add('d-none');\n                this.el.querySelector('div.o_appointment_calendar_form').classList.add('d-none');\n                const timezone = this.el.querySelector('.o_appointment_info_main').dataset.timezone;\n                const upcomingFormattedStart = deserializeDateTime(\n                    upcomingAppointmentData.next_upcoming_appointment.start\n                ).setZone(timezone).toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY);\n                this.el.querySelector('.o_appointment_no_slot_overall_helper').replaceChildren(\n                    renderToElement('Appointment.appointment_info_upcoming_appointment', {\n                        appointmentTypeName: upcomingAppointmentData.next_upcoming_appointment.appointment_type_id[1],\n                        appointmentStart: upcomingFormattedStart,\n                        appointmentToken: upcomingAppointmentData.next_upcoming_appointment.access_token,\n                        partnerId: upcomingAppointmentData.next_upcoming_appointment.appointment_booker_id[0],\n                    }));\n                if (user.userId === false) {\n                    localStorage.setItem('appointment.upcoming_events_access_token', JSON.stringify(upcomingAppointmentData.valid_access_tokens));\n                }\n            } else {\n                localStorage.removeItem('appointment.upcoming_events_access_token');\n            }\n        }\n    },\n\n    /**\n     * Navigate between the months available in the calendar displayed\n     */\n    _onCalendarNavigate: function (ev) {\n        const parentEl = this.el.querySelector(\".o_appointment_month:not(.d-none)\");\n        let monthID = parseInt(parentEl.getAttribute(\"id\").split(\"-\")[1]);\n        monthID += ev.currentTarget.getAttribute(\"id\") === \"nextCal\" ? 1 : -1;\n        parentEl.querySelectorAll(\"table\").forEach((table) => table.classList.remove(\"d-none\"));\n        parentEl\n            .querySelectorAll(\".o_appointment_no_slot_month_helper\")\n            .forEach((element) => element.remove());\n        parentEl.classList.add(\"d-none\");\n        const monthEl = this.el.querySelector(`div#month-${monthID}`);\n        monthEl.classList.remove(\"d-none\");\n        this.el.querySelector(\".active\")?.classList.remove(\"active\");\n        this.slotsListEl.replaceChildren();\n        this.resourceSelectionEl?.replaceChildren();\n\n        if (this.firstEl) {\n            // If there is at least one slot available, check if it is in the current month.\n            if (!monthEl.querySelector(\".o_day\")) {\n                this._renderNoAvailabilityForMonth(monthEl);\n            }\n        }\n    },\n\n    /**\n     * Display the list of slots available for the date selected\n     */\n    _onClickDaySlot: function (ev) {\n        this.el\n            .querySelectorAll(\".o_slot_selected\")\n            .forEach((slot) => slot.classList.remove(\"o_slot_selected\", \"active\"));\n        ev.currentTarget.classList.add(\"o_slot_selected\", \"active\");\n\n        // Do not display slots until user has actively selected the capacity\n        const resourceCapacityEl = this.el.querySelector(\"select[name='resourceCapacity']\");\n        const resourceCapacitySelectedOption =\n            resourceCapacityEl?.options[resourceCapacityEl.selectedIndex];\n        if (\n            resourceCapacitySelectedOption &&\n            resourceCapacitySelectedOption.dataset.placeholderOption\n        ) {\n            return;\n        }\n        const slotDate = ev.currentTarget.dataset.slotDate;\n        const slots = JSON.parse(ev.currentTarget.dataset.availableSlots);\n        const scheduleBasedOn = this.el.querySelector(\"input[name='schedule_based_on']\").value;\n        const resourceAssignMethod = this.el.querySelector(\"input[name='assign_method']\").value;\n        const selectAppointmentResourceEl = this.el.querySelector(\n            \"select[id='selectAppointmentResource']\"\n        );\n        const resourceId =\n            (selectAppointmentResourceEl && selectAppointmentResourceEl.value) ||\n            this.el.querySelector(\"input[name='resource_selected_id']\").value;\n        const resourceCapacity = this.el.querySelector(\"select[name='resourceCapacity']\")?.value;\n        let commonUrlParams = new URLSearchParams(window.location.search);\n        // If for instance the chosen slot is already taken, then an error is thrown and the\n        // user is brought back to the calendar view. In order to keep the selected user, the\n        // url will contain the previously selected staff_user_id (-> preselected in the dropdown\n        // if there is one). If one changes the staff_user in the dropdown, we do not want the\n        // previous one to interfere, hence we delete it. The one linked to the slot is used.\n        // The same is true for duration and date_time used in form rendering.\n        commonUrlParams.delete('staff_user_id');\n        commonUrlParams.delete('resource_selected_id');\n        commonUrlParams.delete('duration');\n        commonUrlParams.delete('date_time');\n        if (resourceCapacity) {\n            commonUrlParams.set('asked_capacity', encodeURIComponent(resourceCapacity));\n        }\n        if (resourceId) {\n            commonUrlParams.set('resource_selected_id', encodeURIComponent(resourceId));\n        }\n\n        this.slotsListEl.replaceChildren(\n            renderToFragment(\"appointment.slots_list\", {\n                commonUrlParams: commonUrlParams,\n                resourceAssignMethod: resourceAssignMethod,\n                scheduleBasedOn: scheduleBasedOn,\n                slotDate: DateTime.fromISO(slotDate).toFormat(\"cccc dd MMMM yyyy\"),\n                slots: slots,\n                getAvailableResources: (slot) => {\n                    return scheduleBasedOn === \"resources\"\n                        ? JSON.stringify(slot[\"available_resources\"])\n                        : false;\n                },\n                getAvailableUsers: (slot) => {\n                    return scheduleBasedOn === \"users\"\n                        ? JSON.stringify(slot[\"available_staff_users\"])\n                        : false;\n                },\n            })\n        );\n        this.resourceSelectionEl?.classList.add(\"d-none\");\n    },\n\n    _onClickHoursSlot: function (ev) {\n        this.el\n            .querySelector(\".o_slot_hours.o_slot_hours_selected\")\n            ?.classList.remove(\"o_slot_hours_selected\", \"active\");\n        ev.currentTarget.classList.add(\"o_slot_hours_selected\", \"active\");\n\n        // If not in 'time_resource' we directly go to the url for the slot\n        // In the case we are in 'time_resource', we don't want to open the link as we want to select a resource\n        // before confirming the slot.\n        const assignMethod = this.el.querySelector(\"input[name='assign_method']\").value;\n        const scheduleBasedOn = this.el.querySelector(\"input[name='schedule_based_on']\").value;\n        if (assignMethod !== \"time_resource\") {\n            const appointmentTypeID = this.el.querySelector(\n                \"input[name='appointment_type_id']\"\n            ).value;\n            const urlParameters = decodeURIComponent(\n                this.el.querySelector(\".o_slot_hours_selected\").dataset.urlParameters\n            );\n            const url = new URL(\n                `/appointment/${encodeURIComponent(appointmentTypeID)}/info?${urlParameters}`,\n                location.origin);\n            document.location = encodeURI(url.href);\n            return;\n        }\n\n        const availableResources = ev.currentTarget.dataset.availableResources\n            ? JSON.parse(ev.currentTarget.dataset.availableResources)\n            : undefined;\n        const availableStaffUsers = ev.currentTarget.dataset.availableStaffUsers\n            ? JSON.parse(ev.currentTarget.dataset.availableStaffUsers)\n            : undefined;\n        const previousResourceIdSelected = this.el.querySelector(\n            \"select[name='resource_id']\"\n        )?.value;\n        this.resourceSelectionEl.replaceChildren(\n            renderToFragment(\"appointment.resources_list\", {\n                availableResources,\n                availableStaffUsers,\n                scheduleBasedOn,\n            })\n        );\n        const availableEntity =\n            scheduleBasedOn === \"resources\" ? availableResources : availableStaffUsers;\n        const resourceIdEl = this.el.querySelector(\"select[name='resource_id']\");\n        if (availableEntity.length === 1) {\n            resourceIdEl.setAttribute(\"disabled\", true);\n        }\n        if (\n            previousResourceIdSelected &&\n            this.el.querySelector(\n                `select[name='resource_id'] > option[value='${previousResourceIdSelected}']`\n            )\n        ) {\n            resourceIdEl.value = previousResourceIdSelected;\n        }\n        this.resourceSelectionEl.classList.remove(\"d-none\");\n    },\n\n    _onClickConfirmSlot: function (ev) {\n        const appointmentTypeID = this.el.querySelector(\"input[name='appointment_type_id']\").value;\n        const resourceId = parseInt(this.el.querySelector(\"select[name='resource_id']\").value);\n        const scheduleBasedOn = this.el.querySelector(\"input[name='schedule_based_on']\").value;\n        const urlParameters = decodeURIComponent(\n            this.el.querySelector(\".o_slot_hours_selected\").dataset.urlParameters\n        );\n        const url = new URL(\n            `/appointment/${encodeURIComponent(appointmentTypeID)}/info?${urlParameters}`,\n            location.origin);\n        const assignMethod = this.el.querySelector(\"input[name='assign_method']\").value;\n        if (scheduleBasedOn === \"resources\") {\n            const resourceCapacity =\n                parseInt(this.el.querySelector(\"select[name='resourceCapacity']\")?.value) || 1;\n            const resourceSelected = this.el.querySelector(\".o_resources_list\").selectedOptions[0];\n            let resourceIds = JSON.parse(url.searchParams.get('available_resource_ids'));\n            if (\n                assignMethod === \"time_resource\" &&\n                parseInt(resourceSelected.dataset.resourceCapacity) >= resourceCapacity\n            ) {\n                resourceIds = [resourceId];\n            }\n            url.searchParams.set('resource_selected_id', encodeURIComponent(resourceId));\n            url.searchParams.set('available_resource_ids', JSON.stringify(resourceIds));\n            url.searchParams.set('asked_capacity', encodeURIComponent(resourceCapacity));\n        } else {\n            url.searchParams.set(\"staff_user_id\", encodeURIComponent(resourceId));\n        }\n        document.location = encodeURI(url.href);\n    },\n\n    _onClickShowCalendar: function (ev) {\n        this.el.querySelector('.o_appointment_no_slot_overall_helper').innerHTML = \"\";\n        this.el.querySelector('div.o_appointment_calendar').classList.remove('d-none');\n        this.el.querySelector('div.o_appointment_calendar_form').classList.remove('d-none');\n        localStorage.setItem(\"appointment.upcoming_events_ignore_until\",\n            serializeDateTime(DateTime.utc().plus({ days: 1 })));\n    },\n\n    /**\n     * Refresh the slots info when the user modifies the timezone or the selected user.\n     */\n    _onRefresh: async function (ev) {\n        if (this.el.querySelector(\"#slots_availabilities\")) {\n            const daySlotSelected =\n                this.el.querySelector(\".o_slot_selected\") &&\n                this.el.querySelector(\".o_slot_selected\").dataset.slotDate;\n            const appointmentTypeID = this.el.querySelector(\n                \"input[name='appointment_type_id']\"\n            ).value;\n            const filterAppointmentTypeIds = this.el.querySelector(\n                \"input[name='filter_appointment_type_ids']\"\n            ).value;\n            const filterUserIds = this.el.querySelector(\n                \"input[name='filter_staff_user_ids']\"\n            ).value;\n            const inviteToken = this.el.querySelector(\"input[name='invite_token']\").value;\n            const previousMonthName = this.el.querySelector(\n                \".o_appointment_month:not(.d-none) .o_appointment_month_name\"\n            )?.textContent;\n            const staffUserID = this.el.querySelector(\n                \"#slots_form select[name='staff_user_id']\"\n            )?.value;\n            const resourceID =\n                this.el.querySelector(\"select[id='selectAppointmentResource']\")?.value ||\n                this.el.querySelector(\"input[name='resource_selected_id']\")?.value;\n            const filterResourceIds = this.el.querySelector(\n                \"input[name='filter_resource_ids']\"\n            ).value;\n            const timezone = this.el.querySelector(\"select[name='timezone']\")?.value;\n            const resourceCapacity =\n                (this.el.querySelector(\"select[name='resourceCapacity']\") &&\n                    parseInt(this.el.querySelector(\"select[name='resourceCapacity']\").value)) ||\n                1;\n            this.el.querySelector(\".o_appointment_no_slot_overall_helper\").replaceChildren();\n            this.slotsListEl.replaceChildren();\n            this.el\n                .querySelectorAll(\"#calendar, .o_appointment_timezone_selection\")\n                .forEach((el) => {\n                    el.classList.add(\"o_appointment_disable_calendar\");\n                });\n            this.resourceSelectionEl?.replaceChildren();\n            const resourceCapacityEl = this.el.querySelector(\"select[name='resourceCapacity']\");\n            const resourceCapacitySelectedOption =\n                resourceCapacityEl?.options[resourceCapacityEl.selectedIndex];\n            if (\n                daySlotSelected &&\n                !(\n                    resourceCapacitySelectedOption &&\n                    resourceCapacitySelectedOption.dataset.placeholderOption\n                )\n            ) {\n                this.el\n                    .querySelector(\".o_appointment_slot_list_loading\")\n                    .classList.remove(\"d-none\");\n            }\n            const updatedAppointmentCalendarHtml = await rpc(\n                `/appointment/${appointmentTypeID}/update_available_slots`,\n                {\n                    asked_capacity: resourceCapacity,\n                    invite_token: inviteToken,\n                    filter_appointment_type_ids: filterAppointmentTypeIds,\n                    filter_staff_user_ids: filterUserIds,\n                    filter_resource_ids: filterResourceIds,\n                    month_before_update: previousMonthName,\n                    resource_selected_id: resourceID,\n                    staff_user_id: staffUserID,\n                    timezone: timezone,\n                }\n            );\n            if (updatedAppointmentCalendarHtml) {\n                this.el.querySelector(\"#slots_availabilities\").outerHTML = updatedAppointmentCalendarHtml;\n                this.initSlots();\n                // If possible, we keep the current month, and display the helper if it has no availability.\n                const displayedMonthEl = this.el.querySelector(\".o_appointment_month:not(.d-none)\");\n                if (!!this.firstEl && !displayedMonthEl.querySelector(\".o_day\")) {\n                    this._renderNoAvailabilityForMonth(displayedMonthEl);\n                }\n                this._removeLoadingSpinner();\n                this.el.querySelector(`div[data-slot-date=\"${daySlotSelected}\"]`)?.click();\n            }\n        }\n    },\n\n    /**\n     * Remove the loading spinners when no longer useful\n     */\n    _removeLoadingSpinner: function () {\n        this.el.querySelector(\".o_appointment_slots_loading\")?.remove();\n        this.el.querySelector(\".o_appointment_slot_list_loading\")?.classList.add(\"d-none\");\n        this.el.querySelector(\"#slots_availabilities\")?.classList.remove(\"d-none\");\n        this.el.querySelectorAll(\"#calendar, .o_appointment_timezone_selection\").forEach((el) => {\n            el.classList.remove(\"o_appointment_disable_calendar\");\n        });\n    },\n});\n", "/** @odoo-module **/\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { findInvalidEmailFromText } from  \"./utils.js\"\nimport { deserializeDateTime } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\n\npublicWidget.registry.appointmentValidation = publicWidget.Widget.extend({\n    selector: '.o_appointment_validation_details',\n    events: {\n        'click .o_appointment_copy_link': '_onCopyVideocallLink',\n        'click .o_appointment_guest_addition_open': '_onGuestAdditionOpen',\n        'click .o_appointment_guest_discard': '_onGuestDiscard',\n        'click .o_appointment_guest_add': '_onGuestAdd',\n    },\n\n    async _onCopyVideocallLink(ev) {\n        const copyButtonEl = ev.target;\n        const tooltip = Tooltip.getOrCreateInstance(copyButtonEl, {\n            title: _t(\"Link Copied!\"),\n            trigger: \"manual\",\n            placement: \"right\",\n        });\n        setTimeout(\n            async () => await browser.navigator.clipboard.writeText(copyButtonEl.dataset.value)\n        );\n        tooltip.show();\n        setTimeout(() => tooltip.hide(), 1200);\n    },\n\n    /**\n     * Store in local storage the appointment booked for the appointment type.\n     * This value is used later to display information on the upcoming appointment\n     * if an appointment is already taken. If the user is logged don't store anything\n     * as everything is computed by the /appointment/get_upcoming_appointments route.\n     * @override\n     */\n    start: function() {\n        return this._super(...arguments).then(() => {\n            if (user.userId) {\n                return;\n            }\n            const eventAccessToken = this.el.dataset.eventAccessToken;\n            const eventStart = this.el.dataset.eventStart && deserializeDateTime(this.el.dataset.eventStart) || false;\n            const allAppointmentsToken = JSON.parse(localStorage.getItem('appointment.upcoming_events_access_token')) || [];\n            if (eventAccessToken && !allAppointmentsToken.includes(eventAccessToken) && eventStart && eventStart > luxon.DateTime.utc()) {\n                allAppointmentsToken.push(eventAccessToken);\n                localStorage.setItem('appointment.upcoming_events_access_token', JSON.stringify(allAppointmentsToken));\n            }\n        });\n    },\n\n    /**\n     * This function will make the RPC call to add the guests from there email,\n     * if a guest is unavailable then it will give us an error msg on the UI side with\n     * the name of the unavailable guest.\n     */\n    _onGuestAdd: async function() {\n        const guestEmails = this.el.querySelector('#o_appointment_input_guest_emails').value;\n        const accessToken = this.el.querySelector('#access_token').value;\n        const emailInfo = findInvalidEmailFromText(guestEmails)\n        if (emailInfo.emailList.length > 10) {\n            this._showErrorMsg(_t('You cannot invite more than 10 people'));\n        } else if (emailInfo.invalidEmails.length) {\n            this._showErrorMsg(_t('Invalid Email'));\n        } else {\n            this._hideErrorMsg();\n            rpc(`/calendar/${accessToken}/add_attendees_from_emails`, {\n                access_token: accessToken,\n                emails_str: guestEmails,\n            }).then(() => location.reload());\n        }\n    },\n\n     /**\n     * This function displays a textarea on the appointment validation page,\n     * allowing users to enter guest emails if the allow_guest option is enabled.\n     */\n     _onGuestAdditionOpen: function(){\n        const textArea = this.el.querySelector('#o_appointment_input_guest_emails');\n        textArea.classList.remove('d-none');\n        textArea.focus();\n        this.el.querySelector('.o_appointment_guest_addition_open').classList.add('d-none');\n        this.el.querySelector('.o_appointment_guest_add').classList.remove('d-none');\n        this.el.querySelector('.o_appointment_guest_discard').classList.remove('d-none')\n    },\n\n    /**\n     * This function will clear the guest email textarea at the appointment validation page\n     * if allow_guest option is enabled.\n     */\n    _onGuestDiscard: function() {\n        this._hideErrorMsg();\n        const textArea = this.el.querySelector('#o_appointment_input_guest_emails');\n        textArea.value = \"\"\n        textArea.classList.add('d-none')\n        this.el.querySelector('.o_appointment_guest_addition_open').classList.remove('d-none');\n        this.el.querySelector('.o_appointment_guest_add').classList.add('d-none');\n        this.el.querySelector('.o_appointment_guest_discard').classList.add('d-none');\n    },\n\n    _hideErrorMsg: function() {\n        const errorMsgDiv = this.el.querySelector('.o_appointment_validation_error');\n        errorMsgDiv.classList.add('d-none');\n    },\n\n    _showErrorMsg: function(errorMessage) {\n        const errorMsgDiv = this.el.querySelector('.o_appointment_validation_error');\n        errorMsgDiv.classList.remove('d-none');\n        errorMsgDiv.querySelector('.o_appointment_error_text').textContent = errorMessage;\n    },\n\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { findInvalidEmailFromText } from  \"./utils.js\"\nimport { _t } from \"@web/core/l10n/translation\";\nimport { addLoadingEffect } from '@web/core/utils/ui';\n\npublicWidget.registry.appointmentForm = publicWidget.Widget.extend({\n    selector: '.o_appointment_attendee_form',\n    events: {\n        'click div.o_appointment_add_guests button.o_appointment_input_guest_add': '_onAddGuest',\n        'click div.o_appointment_add_guests button.o_appointment_input_guest_cancel': '_onHideGuest',\n        'click .o_appointment_form_confirm_btn': '_onConfirmAppointment',\n    },\n\n    /**\n     * Restore the attendee data from the local storage if the attendee doesn't have any partner data.\n     */\n    start: function () {\n        return this._super(...arguments).then(() => {\n            this.hasFormDefaultValues = this._getAttendeeFormData().some(([_, value]) => value !== '');\n            if (!this.hasFormDefaultValues && localStorage.getItem('appointment.form.values')) {\n                const attendeeData = JSON.parse(localStorage.getItem('appointment.form.values'));\n                const form = this.el.querySelector('form.appointment_submit_form');\n                for (const [name, value] of Object.entries(attendeeData)) {\n                    const input = form.querySelector(`input[name=\"${name}\"]`);\n                    if (input) {\n                        input.value = value;\n                    }\n                }\n            }\n        });\n    },\n\n    _getAttendeeFormData: function() {\n        const formData = new FormData(this.el.querySelector('form.appointment_submit_form'));\n        return Array.from(formData).filter(([key]) => ['name', 'phone', 'email'].includes(key));\n    },\n\n    /**\n     * This function will show the guest email textarea where user can enter the\n     * emails of the guests if allow_guests option is enabled.\n     */\n    _onAddGuest: function(){\n        const textArea = this.el.querySelector('#o_appointment_input_guest_emails');\n        textArea.classList.remove('d-none');\n        textArea.focus();\n        const addGuestDiv = this.el.querySelector('div.o_appointment_add_guests')\n        addGuestDiv.querySelector('button.o_appointment_input_guest_add').classList.add('d-none')\n        addGuestDiv.querySelector('button.o_appointment_input_guest_cancel').classList.remove('d-none')\n    },\n\n    _onConfirmAppointment: async function(event) {\n        this._validateCheckboxes();\n        const textArea = this.el.querySelector('#o_appointment_input_guest_emails');\n        const appointmentForm = document.querySelector('.appointment_submit_form');\n        if (textArea && textArea.value.trim() !== '') {\n            let emailInfo = findInvalidEmailFromText(textArea.value);\n            if (emailInfo.invalidEmails.length || emailInfo.emailList.length > 10) {\n                const errorMessage = emailInfo.invalidEmails.length > 0 ? _t('Invalid Email') : _t(\"You cannot invite more than 10 people\");\n                this._showErrorMsg(errorMessage);\n                return;\n            } else {\n                this._hideErrorMsg();\n            }\n        }\n        if (appointmentForm.reportValidity()) {\n            if (!this.hasFormDefaultValues) {\n                const attendeeData = this._getAttendeeFormData();\n                if (attendeeData.length) {\n                    localStorage.setItem('appointment.form.values', JSON.stringify(Object.fromEntries(attendeeData)));\n                }\n            }\n            appointmentForm.submit();\n            addLoadingEffect(event.target);\n        }\n    },\n\n    /**\n     * This function will hide the guest email textarea if allow_guests option is enabled.\n     */\n    _onHideGuest: function() {\n        this._hideErrorMsg();\n        const textArea = this.el.querySelector('#o_appointment_input_guest_emails');\n        textArea.classList.add('d-none')\n        textArea.value = \"\";\n        const addGuestDiv = this.el.querySelector('div.o_appointment_add_guests')\n        addGuestDiv.querySelector('button.o_appointment_input_guest_add').classList.remove('d-none');\n        addGuestDiv.querySelector('button.o_appointment_input_guest_cancel').classList.add('d-none');\n    },\n\n    _hideErrorMsg: function() {\n        const errorMsgDiv = this.el.querySelector('.o_appointment_validation_error');\n        errorMsgDiv.classList.add('d-none');\n    },\n\n    _showErrorMsg: function(errorMessage) {\n        const errorMsgDiv = this.el.querySelector('.o_appointment_validation_error');\n        errorMsgDiv.classList.remove('d-none');\n        errorMsgDiv.querySelector('.o_appointment_error_text').textContent = errorMessage;\n    },\n\n    _validateCheckboxes: function() {\n        this.el.querySelectorAll(\".checkbox-group.required\").forEach((groupEl) => {\n            const checkboxEls = groupEl.querySelectorAll(\".checkbox input\");\n            checkboxEls.forEach(\n                (checkboxEl) =>\n                    (checkboxEl.required = ![...checkboxEls].some(\n                        (checkboxEl) => checkboxEl.checked\n                    ))\n            );\n        });\n    },\n});\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { stepUtils } from \"@web_tour/tour_service/tour_utils\";\n\nimport { markup } from \"@odoo/owl\";\n\nregistry.category(\"web_tour.tours\").add('survey_tour', {\n    url: \"/odoo\",\n    steps: () => [\n    ...stepUtils.goToAppSteps('survey.menu_surveys', markup(_t(\"Ready to change the way you <b>gather data</b>?\"))),\n{\n    trigger: '.btn-outline-primary.o_survey_load_sample',\n    content: markup(_t(\"Load a <b>sample Survey</b> to get started quickly.\")),\n    tooltipPosition: 'left',\n    run: \"click\",\n}, {\n    trigger: 'button[name=action_test_survey]',\n    content: _t(\"Let's give it a spin!\"),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n}, {\n    trigger: 'button[type=submit]',\n    content: _t(\"Let's get started!\"),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n},\n{\n    isActive: [\"auto\"],\n    trigger: '.js_question-wrapper span:contains(\"How frequently\")',\n},\n{\n    trigger: 'button[type=submit]',\n    content: _t(\"Whenever you pick an answer, Odoo saves it for you.\"),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n},\n{\n    isActive: [\"auto\"],\n    trigger: '.js_question-wrapper span:contains(\"How many\")',\n},\n{\n    trigger: 'button[type=submit]',\n    content: _t(\"Only a single question left!\"),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n},\n{\n    isActive: [\"auto\"],\n    trigger: '.js_question-wrapper span:contains(\"How likely\")',\n},\n{\n    trigger: 'button[value=finish]',\n    content: _t(\"Now that you are done, submit your form.\"),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n}, {\n    trigger: '.o_survey_review',\n    content: _t(\"Let's have a look at your answers!\"),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n}, {\n    trigger: '.survey_button_form_view_hook',\n    content: _t(\"Now, use this shortcut to go back to the survey.\"),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n}, {\n    trigger: 'button[name=action_survey_user_input_completed]',\n    content: _t(\"Here, you can overview all the participations.\"),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n}, {\n    trigger: 'td[name=survey_id]',\n    content: _t(\"Let's open the survey you just submitted.\"),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n}, {\n    trigger: '.breadcrumb a:contains(\"Feedback Form\")',\n    content: _t(\"Use the breadcrumbs to quickly go back to the dashboard.\"),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n}\n]});\n", "/** @odoo-module **/\n\nimport PlanningView from '@planning/js/planning_calendar_front';\n\nPlanningView.include({\n        // override popup of calendar\n        eventFunction: function (calEvent) {\n            this._super.apply(this, arguments);\n            const $project = $(\"#project\");\n            if (calEvent.event.extendedProps.project) {\n                $project.text(calEvent.event.extendedProps.project);\n                $project.css(\"display\", \"\");\n                $project.prev().css(\"display\", \"\");\n            } else {\n                $project.css(\"display\", \"none\");\n                $project.prev().css(\"display\", \"none\");\n            }\n            const $task = $(\"#task\");\n            if (calEvent.event.extendedProps.task) {\n                $task.text(calEvent.event.extendedProps.task);\n                $task.prev().css(\"display\", \"\");\n                $task.css(\"display\", \"\");\n            } else {\n                $task.css(\"display\", \"none\");\n                $task.prev().css(\"display\", \"none\");\n            }\n        },\n    });\n", "/** @odoo-module  */\n\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport publicWidget from '@web/legacy/js/public/public_widget';\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { debounce } from \"@web/core/utils/timing\";\n\npublicWidget.registry.knowledgeBaseAutocomplete = publicWidget.Widget.extend({\n    selector: '.o_helpdesk_knowledge_search',\n    events: {\n        'input .search-query': '_onInput',\n        'focusout': '_onFocusOut',\n        'keydown .search-query': '_onKeydown',\n    },\n\n    init: function () {\n        this._super.apply(this, arguments);\n\n        this.keepLast = new KeepLast();\n\n        this._onInput = debounce(this._onInput, 400);\n        this._onFocusOut = debounce(this._onFocusOut, 100);\n    },\n\n\n    start: function () {\n        this.inputEl = this.el.querySelector(\".search-query\");\n        this.url = this.el.dataset.acUrl;\n        this.enabled = parseInt(this.el.dataset.autocomplete);\n\n        return this._super.apply(this, arguments);\n    },\n\n    /**\n     * @private\n     */\n    async _fetch() {\n        const search = this.inputEl.value;\n        if (!search || search.length < 3)\n            return;\n\n        return rpc(this.url, { 'term': search });\n    },\n\n    /**\n     * @private\n     */\n    _render: function (res) {\n        const prevMenuEl = this.menuEl;\n        const search = this.inputEl.value;\n        this.el.classList.toggle(\"dropdown\", !!res);\n        this.el.classList.toggle(\"show\", !!res);\n        if (!!res) {\n            this.menuEl = renderToElement(\"website_helpdesk.knowledge_base_autocomplete\", {\n                results: res.results,\n                showMore: res.showMore,\n                term: search,\n            });\n            this.el.append(this.menuEl);\n        }\n        if (prevMenuEl) {\n            prevMenuEl.remove();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onInput: function () {\n        if (!this.enabled)\n            return;\n        this.keepLast.add(this._fetch()).then(this._render.bind(this));\n    },\n    /**\n     * @private\n     */\n    _onFocusOut: function () {\n        if (!this.el.contains(document.activeElement)) {\n            this._render();\n        }\n    },\n    /**\n     * @private\n     */\n    _onKeydown: function (ev) {\n        switch (ev.key) {\n            case \"Escape\":\n                this._render();\n                break;\n            case \"ArrowUp\":\n            case \"ArrowDown\":\n                ev.preventDefault();\n                if (this.menuEl) {\n                    const element =\n                        ev.key === \"ArrowUp\"\n                            ? this.menuEl.lastElementChild\n                            : this.menuEl.firstElementChild;\n                    element.focus();\n                }\n                break;\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    registerBackendAndFrontendTour,\n} from '@website/js/tours/tour_utils';\n\nregisterBackendAndFrontendTour(\"question\", {\n    url: '/forum/1',\n}, () => [{\n    trigger: \".o_wforum_ask_btn\",\n    tooltipPosition: \"left\",\n    content: _t(\"Create a new post in this forum by clicking on the button.\"),\n    run: \"click\",\n}, {\n    trigger: \"input[name=post_name]\",\n    tooltipPosition: \"top\",\n    content: _t(\"Give your post title.\"),\n    run: \"edit Test\",\n},\n{\n    isActive: [\"auto\"],\n    trigger: `input[name=post_name]:not(:empty)`,\n},\n{\n    trigger: \".note-editable p\",\n    content: _t(\"Put your question here.\"),\n    tooltipPosition: \"bottom\",\n    run: \"editor Test\",\n},\n{\n    isActive: [\"auto\"],\n    trigger: `.note-editable p:not(:contains(/^<br>$/))`,\n},\n{\n    trigger: \".o_select_menu_toggler\",\n    content: _t(\"Insert tags related to your question.\"),\n    tooltipPosition: \"top\",\n    run: \"click\",\n}, \n{\n    trigger: \".o_select_menu_sticky\",\n    run: \"edit Test\",\n},\n{\n    isActive: [\"auto\"],\n    trigger: `.o_popover input.o_select_menu_sticky:not(:contains(Please enter 2 or more characters))`,\n},\n{\n    content: \"Select found select menu item\",\n    trigger: \".o_popover.o_select_menu_menu .o_select_menu_item span:contains('Test')\",\n    run: 'click',\n},\n{\n    content: \"Close search bar\",\n    trigger: \"body\",\n    run: 'click',\n},\n{\n    trigger: \"button:contains(/^Post/)\",\n    content: _t(\"Click to post your question.\"),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n}, {\n    isActive: [\"auto\"],\n    trigger: \".modal .modal-header button.btn-close\",\n    run: \"click\",\n},\n{\n    trigger: \"a:contains(\\\"Reply\\\").collapsed\",\n    content: _t(\"Click to reply.\"),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n},\n{\n    trigger: \".note-editable p\",\n    content: _t(\"Put your answer here.\"),\n    tooltipPosition: \"bottom\",\n    run: \"editor Test\",\n},\n{\n    isActive: [\"auto\"],\n    trigger: `.note-editable p:not(:contains(/^<br>$/))`,\n},\n{\n    trigger: \"button:contains(\\\"Post Answer\\\")\",\n    content: _t(\"Click to post your answer.\"),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n}, {\n    isActive: [\"auto\"],\n    trigger: \".modal .modal-header button.btn-close\",\n    run: \"click\",\n}, {\n    trigger: \".o_wforum_validate_toggler[data-karma]:first\",\n    content: _t(\"Click here to accept this answer.\"),\n    tooltipPosition: \"right\",\n    run: \"click\",\n}, {\n    isActive: [\"auto\"],\n    content: \"Check edit button is there\",\n    trigger: \"a:contains('Edit your answer')\",\n}]);\n", "/** @odoo-module **/\nimport { Component, useState, markup, onWillStart } from \"@odoo/owl\";\nimport { FlagMarkAsOffensiveDialog } from \"../components/flag_mark_as_offensive/flag_mark_as_offensive\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { cookie } from \"@web/core/browser/cookie\";;\nimport { loadWysiwygFromTextarea } from \"@web_editor/js/frontend/loadWysiwygFromTextarea\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { session } from \"@web/session\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { get } from \"@web/core/network/http_service\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { scrollTo, closestScrollable } from \"@web_editor/js/common/scrolling\";\nimport { attachComponent } from \"@web_editor/js/core/owl_utils\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nclass WebsiteForumTagsWrapper extends Component {\n    static template = \"website_forum.WebsiteForumTagsWrapper\";\n    static components = { SelectMenu, DropdownItem };\n    static defaultProps = {\n        isReadOnly: false,\n    };\n    static props = {\n        defaulValue: { optional: true, type: Array },\n        isReadOnly: { optional: true, Type: Boolean },\n    };\n\n    setup() {\n        this.state = useState({\n            value: this.props.defaulValue || [],\n        });\n        onWillStart(async () => {\n            await this.loadChoices();\n        });\n    }\n\n    get showCreateOption() {\n        // The \"Create\" option should not be visible if:\n        // 1. Tag length is less than 2.\n        // 2. The tag already exists (tags are created on form submission, so\n        // consider the current value).\n        // 3. There is insufficient karma.\n        const searchValue = this.select.data.searchValue;\n        const karma = document.querySelector(\"#karma\").value;\n        const editKarma = document.querySelector(\"#karma_edit_retag\").value;\n        const hasEnoughKarma = parseInt(karma) >= parseInt(editKarma);\n\n        return hasEnoughKarma && searchValue.length >= 2\n            && !this.state.choices.some(c => c.label === searchValue)\n            && !this.state.value.some(v => v === `_${searchValue.trim()}`);\n    }\n\n    onCreateOption(string) {\n        const choice = {\n            label: string.trim(),\n            value: `_${string.trim()}`,\n        };\n        this.state.choices.push(choice);\n        this.onSelect([...this.state.value, choice.value]);\n    }\n\n    onSelect(values) {\n        this.state.value = values;\n    }\n\n    async loadChoices(searchString = \"\") {\n        const forumID = document.querySelector(\"#wrapwrap\").dataset.forum_id;\n        const choices = await new Promise((resolve, reject) => {\n            get(`/forum/get_tags?query=${searchString}&limit=${50}&forum_id=${forumID}`).then(\n                (result) => {\n                    result.forEach((choiceEl) => {\n                        choiceEl.value = choiceEl.id;\n                        choiceEl.label = choiceEl.name;\n                    });\n                    resolve(result);\n                }\n            );\n        });\n        this.state.choices = choices;\n    }\n}\n\npublicWidget.registry.websiteForum = publicWidget.Widget.extend({\n    selector: '.website_forum',\n    events: {\n        'click .karma_required': '_onKarmaRequiredClick',\n        'mouseenter .o_js_forum_tag_follow': '_onTagFollowBoxMouseEnter',\n        'mouseleave .o_js_forum_tag_follow': '_onTagFollowBoxMouseLeave',\n        \"click .o_js_forum_tag_follow\": \"_onTagFollowClick\",\n        'mouseenter .o_forum_user_info': '_onUserInfoMouseEnter',\n        'mouseleave .o_forum_user_info': '_onUserInfoMouseLeave',\n        'mouseleave .o_forum_user_bio_expand': '_onUserBioExpandMouseLeave',\n        'click .o_wforum_flag:not(.karma_required)': '_onFlagAlertClick',\n        'click .o_wforum_flag_validator': '_onFlagValidatorClick',\n        'click .o_wforum_flag_mark_as_offensive': '_onFlagMarkAsOffensiveClick',\n        'click .vote_up:not(.karma_required), .vote_down:not(.karma_required)': '_onVotePostClick',\n        'click .o_wforum_validation_queue a[href*=\"/validate\"]': '_onValidationQueueClick',\n        'click .o_wforum_validate_toggler:not(.karma_required)': '_onAcceptAnswerClick',\n        'click .o_wforum_favourite_toggle': '_onFavoriteQuestionClick',\n        'click .comment_delete:not(.karma_required)': '_onDeleteCommentClick',\n        'click .js_close_intro': '_onCloseIntroClick',\n        'click .answer_collapse': '_onExpandAnswerClick',\n        'submit .js_wforum_submit_form:has(:not(.karma_required).o_wforum_submit_post)': '_onSubmitForm',\n    },\n\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n        this.notification = this.bindService(\"notification\");\n    },\n\n    /**\n     * @override\n     */\n    async start() {\n        var self = this;\n        const _super = this._super.bind(this);\n\n        this.lastsearch = [];\n\n        // float-start class messes up the post layout OPW 769721\n        $('span[data-oe-model=\"forum.post\"][data-oe-field=\"content\"]').find('img.float-start').removeClass('float-start');\n\n        // welcome message action button\n        var forumLogin = `${window.location.origin}/odoo?redirect=${encodeURIComponent(window.location.href)}`\n        $('.forum_register_url').attr('href', forumLogin);\n\n        // Initialize forum's tooltips\n        this.$('[data-bs-toggle=\"tooltip\"]').tooltip({delay: 0});\n        this.$('[data-bs-toggle=\"popover\"]').popover({offset: '8'});\n\n        const selectMenuWrapperEl = document.querySelector(\"div.js_select_menu_wrapper\");\n        if (selectMenuWrapperEl) {\n            const isReadOnly = Boolean(selectMenuWrapperEl.dataset.readonly);\n            // Take default tags from the input value\n            const defaulValue = JSON.parse(selectMenuWrapperEl.dataset.initValue || \"[]\").map((x) => x.id);\n\n            await attachComponent(this, selectMenuWrapperEl, WebsiteForumTagsWrapper, {\n                defaulValue: defaulValue,\n                disabled: isReadOnly,\n            });\n        }\n\n        $('textarea.o_wysiwyg_loader').toArray().forEach((textarea) => {\n            var $textarea = $(textarea);\n            var editorKarma = $textarea.data('karma') || 0; // default value for backward compatibility\n            var $form = $textarea.closest('form');\n            var hasFullEdit = parseInt($(\"#karma\").val()) >= editorKarma;\n            var options = {\n                toolbarTemplate: 'website_forum.web_editor_toolbar',\n                toolbarOptions: {\n                    showColors: false,\n                    showFontSize: false,\n                    showHistory: true,\n                    showHeading1: false,\n                    showHeading2: false,\n                    showHeading3: false,\n                    showLink: hasFullEdit,\n                    showImageEdit: hasFullEdit,\n                },\n                recordInfo: {\n                    context: self._getContext(),\n                    res_model: 'forum.post',\n                    // Id is retrieved from URL, which is either:\n                    // - /forum/name-1/post/something-5\n                    // - /forum/name-1/post/something-5/edit\n                    // TODO: Make this more robust.\n                    res_id: +window.location.pathname.split('-').slice(-1)[0].split('/')[0],\n                },\n                value: $textarea.get(0).getAttribute(\"content\"),\n                resizable: true,\n                userGeneratedContent: true,\n                height: 350,\n            };\n            options.allowCommandLink = hasFullEdit;\n            options.allowCommandImage = hasFullEdit;\n            loadWysiwygFromTextarea(self, $textarea[0], options).then(wysiwyg => {\n                // float-start class messes up the post layout OPW 769721\n                $form.find('.note-editable').find('img.float-start').removeClass('float-start');\n            });\n        });\n\n        this.$('.o_wforum_bio_popover').toArray().forEach((authorBox) => {\n            $(authorBox).popover({\n                trigger: 'hover',\n                offset: '10',\n                animation: false,\n                html: true,\n                customClass: 'o_wforum_bio_popover_container shadow-sm',\n            });\n        });\n\n        this.$('#post_reply').on('shown.bs.collapse', function (e) {\n            const replyEl = document.querySelector('#post_reply');\n            const scrollingElement = closestScrollable(replyEl.parentNode);\n            scrollTo(replyEl, {\n                forcedOffset: $(scrollingElement).innerHeight() - $(replyEl).innerHeight(),\n            });\n        });\n        document.querySelectorAll('.o_wforum_question, .o_wforum_answer, .o_wforum_post_comment, .o_wforum_last_activity')\n            .forEach((post) => {\n                post.querySelector('.o_wforum_relative_datetime').textContent = luxon.DateTime\n                    .fromSQL(post.dataset.lastActivity, {zone: 'utc'})\n                    .toRelative();\n            });\n        return _super(...arguments);\n    },\n\n    /**\n     * Check if the user is public, if it's true send a warning alert saying the action cannot be performed.\n     **/\n    _warnIfPublicUser: function() {\n        if (session.is_website_user) {\n            this._displayAccessDeniedNotification(\n                markup(_t('Oh no! Please <a href=\"%s\">sign in</a> to perform this action', \"/web/login\"))\n            );\n            return true;\n        }\n        return false;\n    },\n\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     *\n     * @override\n     * @param {Event} ev\n     */\n    _onSubmitForm: function (ev) {\n        let validForm = true;\n\n        let $form = $(ev.currentTarget);\n        let $title = $form.find('input[name=post_name]');\n        let $textarea = $form.find('textarea[name=content]');\n        // It's not really in the textarea that the user write at first\n        const fillableTextAreaEl = $form[0].querySelector(\".o_wysiwyg_textarea_wrapper\");\n        const isTextAreaFilled = fillableTextAreaEl &&\n            (fillableTextAreaEl.innerText.trim() || fillableTextAreaEl.querySelector(\"img\"));\n\n        if ($title.length && $title[0].required) {\n            if ($title.val()) {\n                $title.removeClass('is-invalid');\n            } else {\n                $title.addClass('is-invalid');\n                validForm = false;\n            }\n        }\n\n        // Because the textarea is hidden, we add the red or green border to its container\n        if ($textarea[0] && $textarea[0].required) {\n            let $textareaContainer = $form.find('.o_wysiwyg_textarea_wrapper');\n            if (!isTextAreaFilled) {\n                $textareaContainer.addClass('border border-danger rounded-top');\n                validForm = false;\n            } else {\n                $textareaContainer.removeClass('border border-danger rounded-top');\n            }\n        }\n\n        if (validForm) {\n            // Stores social share data to display modal on next page.\n            if ($form.has('.oe_social_share_call').length) {\n                sessionStorage.setItem('social_share', JSON.stringify({\n                    targetType: $(ev.currentTarget).find('.o_wforum_submit_post').data('social-target-type'),\n                }));\n            }\n        } else {\n            ev.preventDefault();\n            setTimeout(function() {\n                var $buttons = $(ev.currentTarget).find('button[type=\"submit\"], a.a-submit');\n                $buttons.toArray().forEach((btn) => {\n                    let $btn = $(btn);\n                    $btn.find('i').remove();\n                    $btn.prop('disabled', false);\n                });\n            }, 0);\n        }\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onExpandAnswerClick: function (ev) {\n        const expandableWindow = ev.currentTarget;\n        if (ev.target.matches('.o_wforum_expand_toggle')) {\n            expandableWindow.classList.toggle('o_expand')\n            expandableWindow.classList.toggle('min-vh-100');\n            expandableWindow.classList.toggle('w-lg-50');\n        } else if (ev.target.matches('.o_wforum_discard_btn')){\n            expandableWindow.classList.remove('o_expand', 'min-vh-100');\n            expandableWindow.classList.add('w-lg-50');\n        }\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onKarmaRequiredClick: function (ev) {\n        const karma = parseInt(ev.currentTarget.dataset.karma);\n        if (!karma) {\n            return;\n        }\n        ev.preventDefault();\n        if (this._warnIfPublicUser()) {\n            return;\n        }\n        const forumId = parseInt(document.getElementById('wrapwrap').dataset.forum_id);\n        const additionalInfoWithForumID = forumId\n            ? markup(`<br/>\n                <a class=\"alert-link\" href=\"/forum/${forumId}/faq\">\n                    ${_t(\"Read the guidelines to know how to gain karma.\")}\n                </a>`)\n            : \"\";\n        const translatedText = _t(\"karma is required to perform this action. \");\n        const message = markup(`${karma} ${translatedText}${additionalInfoWithForumID}`);\n        this.notification.add(message, {\n            type: \"warning\",\n            sticky: false,\n            title: _t(\"Karma Error\"),\n        });\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onTagFollowBoxMouseEnter: function (ev) {\n        $(ev.currentTarget).find('.o_forum_tag_follow_box').stop().fadeIn().css('display', 'block');\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onTagFollowBoxMouseLeave: function (ev) {\n        $(ev.currentTarget).find('.o_forum_tag_follow_box').stop().fadeOut().css('display', 'none');\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onTagFollowClick: function (ev) {\n        const closestBtn = ev.target.closest(\"button\");\n        if (closestBtn) {\n            ev.currentTarget.querySelector(\".o_js_forum_tag_link\").classList.toggle(\"text-muted\");\n        }\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onUserInfoMouseEnter: function (ev) {\n        $(ev.currentTarget).parent().find('.o_forum_user_bio_expand').delay(500).toggle('fast');\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onUserInfoMouseLeave: function (ev) {\n        $(ev.currentTarget).parent().find('.o_forum_user_bio_expand').clearQueue();\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onUserBioExpandMouseLeave: function (ev) {\n        $(ev.currentTarget).fadeOut('fast');\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onFlagAlertClick: function (ev) {\n        ev.preventDefault();\n        if (this._warnIfPublicUser()) {\n            return;\n        }\n        const elem = ev.currentTarget;\n        rpc(\n            elem.dataset.href || (elem.getAttribute('href') !== '#' && elem.getAttribute('href')) || elem.closest('form').getAttribute('action'),\n        ).then(data => {\n            if (data.error) {\n                const message = data.error === 'post_already_flagged'\n                    ? _t(\"This post is already flagged\")\n                    : data.error === 'post_non_flaggable'\n                        ? _t(\"This post can not be flagged\")\n                        : data.error;\n                this._displayAccessDeniedNotification(message);\n            } else if (data.success) {\n                const child = elem.firstElementChild;\n                if (data.success === 'post_flagged_moderator') {\n                    const countFlaggedPosts = this.el.querySelector('#count_posts_queue_flagged');\n                    elem.innerText = _t(' Flagged');\n                    elem.prepend(child);\n                    if (countFlaggedPosts) {\n                        countFlaggedPosts.classList.remove('bg-light');\n                        countFlaggedPosts.classList.remove('d-none');\n                        countFlaggedPosts.classList.add('text-bg-danger');\n                        countFlaggedPosts.innerText = parseInt(countFlaggedPosts.innerText, 10) + 1;\n                    }\n                    $(elem).nextAll('.flag_validator').removeClass('d-none');\n                } else if (data.success === 'post_flagged_non_moderator') {\n                    elem.innerText = _t(' Flagged');\n                    elem.prepend(child);\n                    const $forumAnswer = $(elem).closest('.o_wforum_answer');\n                    if ($forumAnswer) {\n                        $forumAnswer.fadeIn(1000);\n                        $forumAnswer.slideUp(1000);\n                    }\n                }\n            }\n        });\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onVotePostClick: function (ev) {\n        ev.preventDefault();\n        if (this._warnIfPublicUser()) {\n            return;\n        }\n        var $btn = $(ev.currentTarget);\n        rpc($btn.data('href')).then(data => {\n            if (data.error) {\n                const message = data.error === 'own_post' ? _t('Sorry, you cannot vote for your own posts') : data.error;\n                this._displayAccessDeniedNotification(message);\n            } else {\n                var $container = $btn.closest('.vote');\n                var $items = $container.children();\n                var $voteUp = $items.filter('.vote_up');\n                var $voteDown = $items.filter('.vote_down');\n                var $voteCount = $items.filter('.vote_count');\n                var userVote = parseInt(data['user_vote']);\n\n                $voteUp.prop('disabled', userVote === 1);\n                $voteDown.prop('disabled', userVote === -1);\n\n                $items.removeClass('text-success text-danger text-muted opacity-75 o_forum_vote_animate');\n                void $container[0].offsetWidth; // Force a refresh\n\n                if (userVote === 1) {\n                    $voteUp.addClass('text-success');\n                    $voteCount.addClass('text-success');\n                    $voteDown.removeClass('karma_required');\n                }\n                if (userVote === -1) {\n                    $voteDown.addClass('text-danger');\n                    $voteCount.addClass('text-danger');\n                    $voteUp.removeClass('karma_required');\n                }\n                if (userVote === 0) {\n                    $voteCount.addClass('text-muted opacity-75');\n                    if (!$voteDown.data('can-downvote')) {\n                        $voteDown.addClass('karma_required');\n                    }\n                    if (!$voteUp.data('can-upvote')) {\n                        $voteUp.addClass('karma_required');\n                    }\n                }\n                $voteCount.html(data['vote_count']).addClass('o_forum_vote_animate');\n            }\n        });\n    },\n    /**\n     * Call the route to moderate/validate the post, then hide the validated post\n     * and decrement the count in the appropriate queue badge of the sidebar on success.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onValidationQueueClick: async function (ev) {\n        ev.preventDefault();\n        const approvalLink = ev.currentTarget;\n        const postBeingValidated = this._findParent(approvalLink, '.post_to_validate');\n        if (!postBeingValidated) {\n            return;\n        }\n        postBeingValidated.classList.add('d-none');\n        let ok;\n        try {\n            ok = (await fetch(approvalLink.href)).ok;\n        } catch {\n            // Calling the endpoint like this returns an HTML page. As we can't\n            // extract the error message from that, we disregard it and simply\n            // restore the post's visibility. This __should__ be improved.\n        }\n        if (!ok) {\n            postBeingValidated.classList.remove('d-none');\n            return;\n        }\n        const nbLeftInQueue = Array.from(document.querySelectorAll('.post_to_validate'))\n            .filter(e => window.getComputedStyle(e).display !== 'none')\n            .length;\n        const queueType = document.querySelector('#queue_type').dataset.queueType;\n        const queueCountBadge = document.querySelector(`#count_posts_queue_${queueType}`);\n        queueCountBadge.innerText = nbLeftInQueue;\n        if (!nbLeftInQueue) {\n            document.querySelector('.o_caught_up_alert').classList.remove('d-none');\n            document.querySelector('.o_wforum_btn_filter_tool')?.classList.add('d-none');\n            queueCountBadge.classList.add('d-none');\n        }\n    },\n    _findParent: function (el, selector) {\n        while (el.parentElement && !el.matches(selector)) {\n            el = el.parentElement;\n        }\n        return el.matches(selector) ? el : null;\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onAcceptAnswerClick: async function (ev) {\n        ev.preventDefault();\n        if (this._warnIfPublicUser()) {\n            return;\n        }\n        const link = ev.currentTarget;\n        const target = link.dataset.target;\n        const data = await rpc(link.dataset.href);\n        if (data.error) {\n            const message = data.error === 'own_post' ? _t('Sorry, you cannot select your own posts as best answer') : data.error;\n            this._displayAccessDeniedNotification(message);\n            return;\n        }\n        for (const answer of document.querySelectorAll('.o_wforum_answer')) {\n            const isCorrect = answer.matches(target) ? data : false;\n            const toggler = answer.querySelector('.o_wforum_validate_toggler');\n            toggler.setAttribute('data-bs-original-title', isCorrect ? toggler.dataset.helperDecline : toggler.dataset.helperAccept);\n            const styleForCorrect = isCorrect ? answer.classList.add : answer.classList.remove;\n            const styleForIncorrect = isCorrect ? answer.classList.remove : answer.classList.add;\n            styleForCorrect.call(answer.classList, 'o_wforum_answer_correct', 'my-2', 'mx-n3', 'mx-lg-n2', 'mx-xl-n3', 'py-3', 'px-3', 'px-lg-2', 'px-xl-3');\n            styleForIncorrect.call(toggler.classList, 'opacity-50');\n            const answerBorder = answer.querySelector('div .border-start');\n            styleForCorrect.call(answerBorder.classList, 'border-success');\n            const togglerIcon = toggler.querySelector('.fa');\n            styleForCorrect.call(togglerIcon.classList, 'fa-check-circle', 'text-success');\n            styleForIncorrect.call(togglerIcon.classList, 'fa-check-circle-o');\n            const correctBadge = answer.querySelector('.o_wforum_answer_correct_badge');\n            styleForCorrect.call(correctBadge.classList, 'd-inline');\n            styleForIncorrect.call(correctBadge.classList, 'd-none');\n        }\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onFavoriteQuestionClick: async function (ev) {\n        ev.preventDefault();\n        const link = ev.currentTarget;\n        const data = await rpc(link.dataset.href);\n        link.classList.toggle('opacity-50', !data);\n        link.classList.toggle('opacity-100-hover', !data);\n        const link_icon = link.querySelector('.fa');\n        link_icon.classList.toggle('fa-star-o', !data);\n        link_icon.classList.toggle('o_wforum_gold', data)\n        link_icon.classList.toggle('fa-star', data)\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onDeleteCommentClick: function (ev) {\n        ev.preventDefault();\n        if (this._warnIfPublicUser()) {\n            return;\n        }\n        this.call(\"dialog\", \"add\", ConfirmationDialog, {\n            body: _t(\"Are you sure you want to delete this comment?\"),\n            confirmLabel: _t(\"Delete\"),\n            confirm: () => {\n                const deleteBtn = ev.currentTarget;\n                rpc(deleteBtn.closest(\"form\").attributes.action.value).then(() => {\n                    deleteBtn.closest(\".o_wforum_post_comment\").remove();\n                }).catch((error) => {\n                    this.notification.add(error.data.message, {\n                        title: _t(\"Karma Error\"),\n                        sticky: false,\n                        type: 'warning',\n                    });\n                });\n            },\n            cancel: () => {},\n        });\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onCloseIntroClick: function (ev) {\n        ev.preventDefault();\n        cookie.set('forum_welcome_message', false, 24 * 60 * 60 * 365, 'optional');\n        $('.forum_intro').slideUp();\n        return true;\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    async _onFlagValidatorClick(ev) {\n        ev.preventDefault();\n        const currentTarget = ev.currentTarget;\n        await this.orm.call(\"forum.post\", currentTarget.dataset.action, [\n            parseInt(currentTarget.dataset.postId),\n        ]);\n        this._findParent(currentTarget, '.o_wforum_flag_alert')?.classList.toggle('d-none');\n        const flaggedButton = currentTarget.parentElement.firstElementChild,\n            child = flaggedButton.firstElementChild,\n            countFlaggedPosts = this.el.querySelector('#count_posts_queue_flagged'),\n            count = parseInt(countFlaggedPosts.innerText, 10) - 1;\n\n        flaggedButton.innerText = _t(' Flag');\n        flaggedButton.prepend(child);\n        if (count === 0) {\n            countFlaggedPosts.classList.add('bg-light');\n        }\n        countFlaggedPosts.innerText = count;\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    async _onFlagMarkAsOffensiveClick(ev) {\n        ev.preventDefault();\n        const template = await rpc($(ev.currentTarget).data('action'));\n        this.call(\"dialog\", \"add\", FlagMarkAsOffensiveDialog, {\n            title: _t(\"Offensive Post\"),\n            body: markup(template),\n        });\n    },\n    _displayAccessDeniedNotification(message) {\n        this.notification.add(message, {\n            title: _t('Access Denied'),\n            sticky: false,\n            type: 'warning',\n        });\n    }\n});\n\npublicWidget.registry.websiteForumSpam = publicWidget.Widget.extend({\n    selector: '.o_wforum_moderation_queue',\n    events: {\n        'click .o_wforum_select_all_spam': '_onSelectallSpamClick',\n        'click .o_wforum_mark_spam': 'async _onMarkSpamClick',\n        'input #spamSearch': '_onSpamSearchInput',\n    },\n\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n    },\n\n    /**\n     * @override\n     */\n    start: function () {\n        this.spamIDs = this.$('.modal').data('spam-ids');\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onSelectallSpamClick: function (ev) {\n        var $spamInput = this.$('.modal .tab-pane.active input');\n        $spamInput.prop('checked', true);\n    },\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onSpamSearchInput: function (ev) {\n        var self = this;\n        var toSearch = $(ev.currentTarget).val();\n        return this.orm.searchRead(\n            \"forum.post\",\n            [['id', 'in', self.spamIDs],\n                '|',\n                ['name', 'ilike', toSearch],\n                ['content', 'ilike', toSearch]],\n            ['name', 'content']\n        ).then(function (o) {\n            Object.values(o).forEach((r) => {\n                r.content = $('<p>' + $(r.content).html() + '</p>').text().substring(0, 250);\n            });\n            self.$('div.post_spam').empty().append(renderToElement('website_forum.spam_search_name', {\n                posts: o,\n            }));\n        });\n    },\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onMarkSpamClick: function (ev) {\n        var key = this.$('.modal .tab-pane.active').data('key');\n        var $inputs = this.$('.modal .tab-pane.active input.form-check-input:checked');\n        var values = Array.from($inputs).map((o) => parseInt(o.value));\n        return this.orm.call(\"forum.post\", \"mark_as_offensive_batch\", [\n            this.spamIDs,\n            key,\n            values,\n        ]).then(function () {\n            window.location.reload();\n        });\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport \"@website/js/content/snippets.animation\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nconst ForumShare = publicWidget.Widget.extend({\n    selector: '',\n    events: {},\n\n    /**\n     * @override\n     * @param {Object} parent\n     * @param {Object} options\n     * @param {string} targetType\n     */\n    init: function (parent, options, targetType) {\n        this._super.apply(this, arguments);\n        this.targetType = targetType;\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        var def = this._super.apply(this, arguments);\n        var $question = this.$('article.question');\n        if (!this.targetType) {\n            this._super.apply(this, arguments);\n        } else {\n            const el = renderToElement('website.social_modal', {\n                target_type: this.targetType,\n                state: $question.data('state'),\n            });\n            $('body').append(el);\n            this.trigger_up('widgets_start_request', {\n                editableMode: false,\n                $target: $(el.querySelector(\".s_share\")),\n            });\n            $('#oe_social_share_modal').modal('show');\n        }\n        return def;\n    },\n});\n\npublicWidget.registry.websiteForumShare = publicWidget.Widget.extend({\n    selector: '.website_forum',\n\n    /**\n     * @override\n     */\n    start: function () {\n        // Retrieve stored social data\n        if (sessionStorage.getItem('social_share')) {\n            var socialData = JSON.parse(sessionStorage.getItem('social_share'));\n            (new ForumShare(this, false, socialData.targetType)).attachTo($(document.body));\n            sessionStorage.removeItem('social_share');\n        }\n\n        return this._super.apply(this, arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport { Component, useEffect } from \"@odoo/owl\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nexport class FlagMarkAsOffensiveDialog extends Component {\n    static template = \"website_forum.FlagMarkAsOffensiveDialog\";\n    static components = { Dialog };\n    static props = {\n        title: String,\n        body: String,\n        close: Function,\n    };\n\n    setup() {\n        this.modalRef = useChildRef();\n\n        const onClickDiscard = (ev) => {\n            ev.preventDefault();\n            this.props.close();\n        };\n\n        useEffect(\n            (discardButton) => {\n                if (discardButton) {\n                    discardButton.addEventListener(\"click\", onClickDiscard);\n                    return () => {\n                        discardButton.removeEventListener(\"click\", onClickDiscard);\n                    };\n                }\n            },\n            () => [this.modalRef.el?.querySelector(\".btn-link\")]\n        );\n    }\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { escape, sprintf } from \"@web/core/utils/strings\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState, onWillStart, markup, useRef } from \"@odoo/owl\";\n\nexport class CreateTicketDialog extends Component {\n    static template = \"website_helpdesk_forum.CreateTicketDialog\";\n    static components = { Dialog };\n    static props = {\n        forumId: { type: Number, optional: false },\n        postId: { type: Number, optional: false },\n        close: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.state = useState({});\n        this.inputText = useRef(\"inputText\");\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n\n        onWillStart(async () => {\n            const forumPostData = await rpc(window.location.href + \"/get-forum-data\");\n            this.state.data = {\n                ...forumPostData,\n                team_id: forumPostData.teams?.[0]?.[0],\n            };\n        });\n    }\n\n    _createTicket() {\n        return this.orm.call('forum.forum', 'create_ticket', [\n            this.props.forumId,\n            this.props.postId,\n            this.state.data,\n        ]);\n    }\n\n    _checkInputIsValid() {\n        const isValid = this.inputText.el.value.trim().length;\n        this.inputText.el.classList.toggle('is-invalid', !isValid);\n        return isValid;\n    }\n\n    async onCreateTicket() {\n        if (!this._checkInputIsValid()) {\n            return;\n        }\n        const response = await this._createTicket();\n        const message = markup(sprintf(\n            escape(_t('Helpdesk ticket %s has been successfully created for this forum post.')),\n            `<b>#${escape(response.ticket)}</b>`,\n        ));\n        this.notification.add(message, { type: \"success\" });\n        this.props.close();\n    }\n\n    async onCreateAndViewTicket() {\n        if (!this._checkInputIsValid()) {\n            return;\n        }\n        const response = await this._createTicket();\n        window.open(response.url, '_blank');\n        this.props.close();\n    }\n}\n", "import publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { CreateTicketDialog } from \"../components/create_ticket_dialog/create_ticket_dialog\";\n\npublicWidget.registry.CreateTicket = publicWidget.Widget.extend({\n    selector: '.create_ticket_forum',\n    events: {\n        'click': '_onCreateTicket',\n    },\n\n    /**\n     * @override\n    */\n    init() {\n        this._super(...arguments);\n        this.dialog = this.bindService(\"dialog\");\n    },\n\n    /**\n     * @override\n    */\n    start() {\n        this.forumId = this.$el.data('forumId');\n        this.postId = this.$el.data('postId');\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _onCreateTicket() {\n        this.dialog.add(CreateTicketDialog, {\n            forumId: this.forumId,\n            postId: this.postId,\n        });\n    },\n});\n", "/** @odoo-module **/\n\n    import publicWidget from \"@web/legacy/js/public/public_widget\";\n    import { loadJS } from \"@web/core/assets\";\n    /* global OdooFin */\n\n    publicWidget.registry.OnlineSyncPortal = publicWidget.Widget.extend({\n        selector: '.oe_online_sync',\n        events: Object.assign({}, {\n            'click #renew_consent_button': '_onRenewConsent',\n        }),\n\n        OdooFinConnector: function (parent, action) {\n            // Ensure that the proxyMode is valid\n            const modeRegexp = /^[a-z0-9-_]+$/i;\n            if (!modeRegexp.test(action.params.proxyMode)) {\n                return;\n            }\n            const url = 'https://' + action.params.proxyMode + '.odoofin.com/proxy/v1/odoofin_link';\n\n            loadJS(url)\n                .then(() => {\n                    // Create and open the iframe\n                    const params = {\n                        data: action.params,\n                        proxyMode: action.params.proxyMode,\n                        onEvent: function (event, data) {\n                            switch (event) {\n                                case 'success':\n                                    const processUrl = window.location.pathname + '/complete' + window.location.search;\n                                    $('.js_reconnect').toggleClass('d-none');\n                                    $.post(processUrl, {csrf_token: odoo.csrf_token});\n                                default:\n                                    return;\n                            }\n                        },\n                    };\n                    OdooFin.create(params);\n                    OdooFin.open();\n                });\n            return;\n        },\n\n        /**\n         * @private\n         * @param {Event} ev\n         */\n        _onRenewConsent: async function (ev) {\n            ev.preventDefault();\n            const action = JSON.parse($(ev.currentTarget).attr('iframe-params'));\n            return this.OdooFinConnector(this, action);\n        },\n    });\n\n    export default {\n        OnlineSyncPortal: publicWidget.registry.OnlineSyncPortal,\n    };\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { markup } from \"@odoo/owl\";\nimport { InputConfirmationDialog } from \"@portal/js/components/input_confirmation_dialog/input_confirmation_dialog\";\nimport { handleCheckIdentity } from \"@portal/js/portal_security\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { user } from \"@web/core/user\";\n\n/**\n * Replaces specific <field> elements by normal HTML, strip out the rest entirely\n */\nfunction fromField(f, record) {\n    switch (f.getAttribute('name')) {\n    case 'qrcode':\n        const qrcode = document.createElement('img');\n        qrcode.setAttribute('class', 'img img-fluid');\n        qrcode.setAttribute('src', 'data:image/png;base64,' + record['qrcode']);\n        return qrcode;\n    case 'url':\n        const url = document.createElement('a');\n        url.setAttribute('href', record['url']);\n        url.textContent = f.getAttribute('text') || record['url'];\n        return url;\n    case 'code':\n        const code = document.createElement('input');\n        code.setAttribute('name', 'code');\n        code.setAttribute('class', 'form-control col-10 col-md-6');\n        code.setAttribute('placeholder', '6-digit code');\n        code.required = true;\n        code.maxLength = 6;\n        code.minLength = 6;\n        return code;\n    case 'secret':\n        // As CopyClipboard wizard is backend only, mimic his behaviour to use it in frontend.\n        // Field\n        const secretSpan = document.createElement('span');\n        secretSpan.setAttribute('name', 'secret');\n        secretSpan.setAttribute('class', 'o_field_copy_url');\n        secretSpan.textContent = record['secret'];\n\n        // Copy Button\n        const copySpanIcon = document.createElement('span');\n        copySpanIcon.setAttribute('class', 'fa fa-clipboard');\n        const copySpanText = document.createElement('span');\n        copySpanText.textContent = _t(' Copy');\n\n        const copyButton = document.createElement('button');\n        copyButton.setAttribute('class', 'btn btn-sm btn-primary o_clipboard_button o_btn_char_copy py-0 px-2');\n        copyButton.onclick = async function(event) {\n            event.preventDefault();\n            $(copyButton).tooltip({title: _t(\"Copied!\"), trigger: \"manual\", placement: \"bottom\"});\n            await browser.navigator.clipboard.writeText($(secretSpan)[0].innerText);\n            $(copyButton).tooltip('show');\n            setTimeout(() => $(copyButton).tooltip(\"hide\"), 800);\n        };\n\n        copyButton.appendChild(copySpanIcon);\n        copyButton.appendChild(copySpanText);\n\n        // CopyClipboard Div\n        const secretDiv = document.createElement('div');\n        secretDiv.setAttribute('class', 'o_field_copy d-flex justify-content-center align-items-center');\n        secretDiv.appendChild(secretSpan);\n        secretDiv.appendChild(copyButton);\n\n        return secretDiv;\n    default: // just display the field's data\n        return document.createTextNode(record[f.getAttribute('name')] || '');\n    }\n}\n\n/**\n * Apparently chrome literally absolutely can't handle parsing XML and using\n * those nodes in an HTML document (even when parsing as application/xhtml+xml),\n * this results in broken rendering and a number of things not working (e.g.\n * classes) without any specific warning in the console or anything, things are\n * just broken with no indication of why.\n *\n * So... rebuild the entire f'ing body using document.createElement to ensure\n * we have HTML elements.\n *\n * This is a recursive implementation so it's not super efficient but the views\n * to fixup *should* be relatively simple.\n */\nfunction fixupViewBody(oldNode, record) {\n    let qrcode = null, code = null, node = null;\n\n    switch (oldNode.nodeType) {\n        case 1: // element\n            if (oldNode.tagName === 'field') {\n                node = fromField(oldNode, record);\n                switch (oldNode.getAttribute('name')) {\n                case 'qrcode':\n                    qrcode = node;\n                    break;\n                case 'code':\n                    code = node;\n                    break\n                }\n                break; // no need to recurse here\n            }\n            node = document.createElement(oldNode.tagName);\n            for(let i=0; i<oldNode.attributes.length; ++i) {\n                const attr = oldNode.attributes[i];\n                node.setAttribute(attr.name, attr.value);\n            }\n            for(let j=0; j<oldNode.childNodes.length; ++j) {\n                const [ch, qr, co] = fixupViewBody(oldNode.childNodes[j], record);\n                if (ch) { node.appendChild(ch); }\n                if (qr) { qrcode = qr; }\n                if (co) { code = co; }\n            }\n            break;\n        case 3: case 4: // text, cdata\n            node = document.createTextNode(oldNode.data);\n            break;\n        default:\n            // don't care about PI & al\n    }\n\n    return [node, qrcode, code]\n}\n\npublicWidget.registry.TOTPButton = publicWidget.Widget.extend({\n    selector: '#auth_totp_portal_enable',\n    events: {\n        click: '_onClick',\n    },\n\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n        this.dialog = this.bindService(\"dialog\");\n    },\n\n    async _onClick(e) {\n        e.preventDefault();\n\n        const w = await handleCheckIdentity(\n            this.orm.call(\"res.users\", \"action_totp_enable_wizard\", [user.userId]),\n            this.orm,\n            this.dialog\n        );\n\n        if (!w) {\n            // TOTP probably already enabled, just reload page\n            window.location = window.location;\n            return;\n        }\n\n        const {res_model: model, res_id: wizard_id} = w;\n\n        const record = await this.orm.read(model, [wizard_id], []).then(ar => ar[0]);\n\n        const doc = new DOMParser().parseFromString(\n            document.getElementById('totp_wizard_view').textContent,\n            'application/xhtml+xml'\n        );\n\n        const xmlBody = doc.querySelector('sheet *');\n        const [body, ,] = fixupViewBody(xmlBody, record);\n\n        this.call(\"dialog\", \"add\", InputConfirmationDialog, {\n            body: markup(body.outerHTML),\n            onInput: ({ inputEl }) => {\n                inputEl.setCustomValidity(\"\");\n            },\n            confirmLabel: _t(\"Activate\"),\n            confirm: async ({ inputEl }) => {\n                if (!inputEl.reportValidity()) {\n                    inputEl.classList.add(\"is-invalid\");\n                    return false;\n                }\n\n                try {\n                    await this.orm.write(model, [record.id], { code: inputEl.value });\n                    await handleCheckIdentity(\n                        this.orm.call(model, \"enable\", [record.id]),\n                        this.orm,\n                        this.dialog\n                    );\n                } catch (e) {\n                    const errorMessage = (\n                        !e.message ? e.toString()\n                      : !e.message.data ? e.message.message\n                      : e.message.data.message || _t(\"Operation failed for unknown reason.\")\n                    );\n                    inputEl.classList.add(\"is-invalid\");\n                    // show custom validity error message\n                    inputEl.setCustomValidity(errorMessage);\n                    inputEl.reportValidity();\n                    return false;\n                }\n                // reloads page, avoid window.location.reload() because it re-posts forms\n                window.location = window.location;\n            },\n            cancel: () => {},\n        });\n    },\n});\npublicWidget.registry.DisableTOTPButton = publicWidget.Widget.extend({\n    selector: '#auth_totp_portal_disable',\n    events: {\n        click: '_onClick'\n    },\n\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n        this.dialog = this.bindService(\"dialog\");\n    },\n\n    async _onClick(e) {\n        e.preventDefault();\n        await handleCheckIdentity(\n            this.orm.call(\"res.users\", \"action_totp_disable\", [user.userId]),\n            this.orm,\n            this.dialog\n        )\n        window.location = window.location;\n    }\n});\npublicWidget.registry.RevokeTrustedDeviceButton = publicWidget.Widget.extend({\n    selector: '#totp_wizard_view + * .fa.fa-trash.text-danger',\n    events: {\n        click: '_onClick'\n    },\n\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n        this.dialog = this.bindService(\"dialog\");\n    },\n\n    async _onClick(e){\n        e.preventDefault();\n        await handleCheckIdentity(\n            this.orm.call(\"auth_totp.device\", \"remove\", [parseInt(this.el.id)]),\n            this.orm,\n            this.dialog\n        );\n        window.location = window.location;\n    }\n});\npublicWidget.registry.RevokeAllTrustedDevicesButton = publicWidget.Widget.extend({\n    selector: '#auth_totp_portal_revoke_all_devices',\n    events: {\n        click: '_onClick'\n    },\n\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n        this.dialog = this.bindService(\"dialog\");\n    },\n\n    async _onClick(e){\n        e.preventDefault();\n        await handleCheckIdentity(\n            this.orm.call(\"res.users\", \"revoke_all_devices\", [user.userId]),\n            this.orm,\n            this.dialog\n        );\n        window.location = window.location;\n    }\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { debounce } from \"@web/core/utils/timing\";\n\npublicWidget.registry.websiteEventTrack = publicWidget.Widget.extend({\n    selector: '.o_wevent_event',\n    events: {\n        'input #event_track_search': '_onEventTrackSearchInput',\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    start: function () {\n        this._super.apply(this, arguments).then(() => {\n            this.$el.find('[data-bs-toggle=\"popover\"]').popover();\n\n            this.agendas = Array.from(this.target.getElementsByClassName('o_we_online_agenda'));\n\n            if (this.agendas.length > 0) {\n                this._checkAgendasOverflow(this.agendas);\n                this.agendaScroller = this.$el[0].querySelector('.o_we_agenda_horizontal_scroller_container');\n                this.agendaScrollerElement = this.agendaScroller.querySelector('.o_we_agenda_horizontal_scroller');\n\n                this.agendas.forEach(agenda => {\n                    agenda.addEventListener('scroll', event => {\n                        this._onAgendaScroll(agenda, event);\n                        if(this.agendaScroller && this.visibleAgenda) {\n                            this.agendaScroller.scrollLeft = this.visibleAgenda.scrollLeft;\n                        }\n                    });\n                });\n\n                if (this.agendaScroller) {\n                    this._updateAgendaScroll = debounce(this._updateAgendaScroll, 50);\n                    if (document.querySelector('#wrapwrap.event')) {\n                        window.addEventListener('scroll',\n                            this._updateAgendaScroll.bind(this)\n                        );\n                    }\n\n                    window.addEventListener('resize', () => {\n                        this._updateAgendaScroll();\n                    });\n\n                    this.agendaScroller.addEventListener('scroll', () => {\n                        if (this.visibleAgenda) {\n                            this.visibleAgenda.scrollLeft = this.agendaScroller.scrollLeft;\n                        }\n                    });\n\n                    this._updateAgendaScroll();\n                }\n            }\n        });\n    },\n\n    /**\n     * Dynamic horizontal scrollbar.\n     * It's meant the show up as a sticky scrollbar at the bottom of the screen, to allow scrolling\n     * the agenda horizontally even if you've not reached the bottom of the agenda container.\n     * Makes the user experience much smoother.\n     *\n     * Technically, the code checks \"what is the last agenda on the screen\" and enables our sticky\n     * scrollbar based on that.\n     */\n    _updateAgendaScroll() {\n        // reverse the agendas, we always want the last agenda \"on screen\" to be the scrolled one\n        this.visibleAgenda = this.agendas.toReversed().find((el) => {\n            const rect = el.getBoundingClientRect();\n            let containerOffset = {\n                top: rect.top + window.scrollY + 30,  // some offset for a better experience\n                bottom: rect.bottom + window.scrollY\n            };\n            let windowOffset = {\n                top: window.scrollY,\n                bottom: window.scrollY + window.innerHeight\n            };\n\n            // if the top of the container if visible but NOT the bottom\n            return (containerOffset.top < windowOffset.bottom) &&\n                !(containerOffset.bottom < windowOffset.bottom);\n        });\n\n        if (this.visibleAgenda && this.visibleAgenda.classList.contains('o_we_online_agenda_has_content_hidden')) {\n            // need to account for vertical scrollbar width\n            const verticalScrollbarWidth = window.innerWidth - document.documentElement.clientWidth;\n\n            this.agendaScroller.classList.remove('d-none');\n            this.agendaScrollerElement.style.width = (this.visibleAgenda.scrollWidth + verticalScrollbarWidth) + 'px';\n        } else {\n            this.agendaScroller.classList.add('d-none');\n        }\n    },\n\n    /**\n     * @private\n     * @param {Object} agendas\n     */\n    _checkAgendasOverflow: function (agendas) {\n        agendas.forEach(agendaEl => {\n            const hasScroll = agendaEl.querySelector('table').clientWidth > agendaEl.clientWidth;\n\n            agendaEl.classList.toggle('o_we_online_agenda_has_scroll', hasScroll);\n            agendaEl.classList.toggle('o_we_online_agenda_has_content_hidden', hasScroll);\n        });\n    },\n\n    /**\n     * @private\n     * @param {Object} agendaEl\n     * @param {Event} event\n     */\n    _onAgendaScroll: function (agendaEl, event) {\n        const tableEl = agendaEl.querySelector('table');\n        const gutter = 4; // = map-get($spacers, 1)\n        const gap = tableEl.clientWidth - agendaEl.clientWidth - gutter;\n\n        agendaEl.classList.add('o_we_online_agenda_is_scrolling');\n        agendaEl.classList.toggle('o_we_online_agenda_has_content_hidden', gap > Math.ceil(agendaEl.scrollLeft));\n\n        requestAnimationFrame(() => {\n            setTimeout(() => {\n                agendaEl.classList.remove('o_we_online_agenda_is_scrolling');\n            }, 200);\n        });\n    },\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onEventTrackSearchInput: function (ev) {\n        ev.preventDefault();\n        var text = $(ev.currentTarget).val();\n        var $tracks = $('.event_track');\n\n        //check if the user is performing a search; i.e., text is not empty\n        if (text) {\n            function filterTracks(index, element) {\n                //when filtering elements only check the text content\n                return this.textContent.toLowerCase().includes(text.toLowerCase());\n            }\n            $('#search_summary').removeClass('invisible');\n            $('#search_number').text($tracks.filter(filterTracks).length);\n\n            $tracks.removeClass('invisible').not(filterTracks).addClass('invisible');\n        } else {\n            //if no search is being performed; hide the result count text\n            $('#search_summary').addClass('invisible');\n            $tracks.removeClass('invisible')\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { scrollTo } from \"@web_editor/js/common/scrolling\";\n\npublicWidget.registry.websiteEventTrackProposalForm = publicWidget.Widget.extend({\n    selector: '.o_website_event_track_proposal_form',\n    events: {\n        'click .o_wetrack_add_contact_information_checkbox': '_onAdvancedContactToggle',\n        'input input[name=\"partner_name\"]': '_onPartnerNameInput',\n        'click .o_wetrack_proposal_submit_button': '_onProposalFormSubmit',\n    },\n\n    /**\n     * @override\n     */\n    init: function () {\n        this._super(...arguments);\n        this.useAdvancedContact = false;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Evaluate and return validity of form input fields:\n     * - 1) error 'invalidFormInputs' : Invalid ones are marked as is-invalid and o_wetrack_input_error.\n     * - 2) error 'noContactMean' : Contact mean fields are marked as is-invalid and contact\n     * section as o_wetrack_no_contact_mean_error if none of them is filled.\n     *\n     * @private\n     * @returns {Boolean} - True if no error remain, false otherwise\n     */\n    _isFormValid: function () {\n        var formErrors = [];\n\n        // 1) Valid Form Inputs\n        this.$('.form-control').each(function () {\n            var $formControl = $(this);\n            // Validate current input, if not SelectMenu field.\n            var inputs = $formControl.not(\".o_wetrack_select_tags\");\n            var invalidInputs = inputs.toArray().filter(function (input) {\n                return !input.checkValidity();\n            });\n\n            $formControl.removeClass('o_wetrack_input_error is-invalid');\n            if (invalidInputs.length) {\n                $formControl.addClass('o_wetrack_input_error is-invalid');\n                formErrors.push('invalidFormInputs');\n            }\n        });\n\n        // 2) Advanced Contact Must Have a Contact Mean\n        if (this.useAdvancedContact) {\n            var hasContactMean = this.$('.o_wetrack_contact_phone_input').val() ||\n                this.$('.o_wetrack_contact_email_input').val();\n            if (!hasContactMean) {\n                this.$('.o_wetrack_contact_information').addClass('o_wetrack_no_contact_mean_error');\n                this.$('.o_wetrack_contact_mean').addClass('is-invalid');\n                formErrors.push('noContactMean');\n            } else {\n                this.$('.o_wetrack_contact_information').removeClass('o_wetrack_no_contact_mean_error');\n                this.$('.o_wetrack_contact_mean:not(\".o_wetrack_input_error\")').removeClass('is-invalid');\n            }\n        }\n\n        // Form Validity and Error Display\n        this._updateErrorDisplay(formErrors);\n        return formErrors.length === 0;\n    },\n\n    /**\n     * If there are still errors in form, display the error section and\n     * compose the error message accordingly.\n     *\n     * @private\n     * @param {Array} errors - Names of errors still present in form.\n     */\n    _updateErrorDisplay: function (errors) {\n\n        this.$('.o_wetrack_proposal_error_section').toggleClass('d-none', !errors.length);\n\n        var errorMessages = [];\n        var $errorElement = this.$('.o_wetrack_proposal_error_message');\n\n        if (errors.includes('invalidFormInputs')) {\n            errorMessages.push(_t('Please fill out the form correctly.'));\n        }\n\n        if (errors.includes('noContactMean')) {\n            errorMessages.push(_t('Please enter either a contact email address or a contact phone number.'));\n        }\n\n        if (errors.includes('forbidden')) {\n            errorMessages.push(_t('You cannot access this page.'));\n        }\n\n        $errorElement.text(errorMessages.join(' ')).change();\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Display / Hide Additional Contact Information section when toggling\n     * the checkbox on the form o_wetrack_add_contact_information_checkbox.\n     * Also empty the email to prevent hidden email format error.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onAdvancedContactToggle: function (ev) {\n        this.useAdvancedContact = !this.useAdvancedContact;\n        var $contactName = this.$(\".o_wetrack_contact_name_input\")[0];\n        var $advancedInformation = this.$('.o_wetrack_contact_information');\n\n        if (this.useAdvancedContact) {\n            $advancedInformation.removeClass('d-none');\n            $contactName.setAttribute(\"required\", \"True\");\n        } else {\n            this.$('.o_wetrack_contact_email_input').val('').change();\n            $advancedInformation.addClass('d-none');\n            $contactName.removeAttribute(\"required\");\n        }\n    },\n\n    /**\n     * Propagates the new input on speaker name to contact name, as long as the latter\n     * is the start of partner name. Otherwise, do not modify existing contact name.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onPartnerNameInput: function (ev) {\n        var partnerNameText = $(ev.currentTarget).val();\n        var contactNameText = this.$(\".o_wetrack_contact_name_input\").val();\n        if (partnerNameText.startsWith(contactNameText)) {\n            this.$(\".o_wetrack_contact_name_input\").val(partnerNameText).change();\n        }\n    },\n\n    /**\n     * Submits the form if no errors are present in the form after validation.\n     *\n     * If the submission succeeds, we replace the form with a template containing a small success\n     * message.\n     *\n     * Then we scroll to the position of the success message so that the user can see it.\n     * To do that we have to compute the position of the beginning of the element, relatively to its\n     * position and the amount already scrolled, then subtract the floating header menu.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onProposalFormSubmit: async function (ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n\n        // Prevent further clicking\n        this.$el.find('.o_wetrack_proposal_submit_button')\n            .addClass('disabled')\n            .attr('disabled', 'disabled');\n\n        // Submission of the form if no errors remain\n        if (this._isFormValid()) {\n            const formData = new FormData(this.$el[0]);\n\n            const response = await $.ajax({\n                url: `/event/${encodeURIComponent(this.$el.data('eventId'))}/track_proposal/post`,\n                data: formData,\n                processData: false,\n                contentType: false,\n                type: 'POST'\n            });\n\n            const jsonResponse = response && JSON.parse(response);\n            if (jsonResponse.success) {\n                // TODO we really should not remove the whole widget element\n                // like that + probably restore the widget before edit mode etc.\n                const parentEl = this.el.parentNode;\n                this.$el.replaceWith($(renderToElement('event_track_proposal_success')));\n                scrollTo(parentEl, { extraOffset: 20, duration: 50 });\n            } else if (jsonResponse.error) {\n                this._updateErrorDisplay([jsonResponse.error]);\n            }\n        }\n\n        // Restore button\n        this.$el.find('.o_wetrack_proposal_submit_button')\n            .removeAttr('disabled')\n            .removeClass('disabled');\n    },\n});\n\nexport default publicWidget.registry.websiteEventTrackProposalForm;\n", "import { Component, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { attachComponent } from \"@web_editor/js/core/owl_utils\";\n\nclass WebsiteEventTrackProposalFormTagsWrapper extends Component {\n    static template = \"website_event_track.WebsiteEventTrackProposalFormTagsWrapper\";\n    static components = { SelectMenu };\n    static props = {\n        placeholder: { optional: true, type: String },\n        defaultChoices: { optional: true, type: Array },\n    };\n\n    setup() {\n        this.state = useState({\n            ...this.props,\n            value: [],\n        });\n    }\n    onSelect(item) {\n        this.state.value = item;\n    }\n}\n\npublicWidget.registry.websiteEventTrackProposalFormTags = publicWidget.Widget.extend({\n    selector: '.o_website_event_track_proposal_form_tags',\n\n    async willStart() {\n        const choices = await rpc(\"/event/track_tag/search_read\", {\n            fields: [\"id\", \"name\", \"category_id\"],\n            domain: [],\n        });\n        this.choices = choices.map(({ id, category_id, name }) => {\n            return {\n                value: id,\n                label: category_id ? `${category_id[1]} : ${name}` : name,\n            };\n        });\n    },\n\n    async start() {\n        await this._super(...arguments);\n        await attachComponent(this, this.el, WebsiteEventTrackProposalFormTagsWrapper, {\n            defaultChoices: this.choices || [],\n            placeholder: _t(\"Select Categories\"),\n        });\n    },\n});\n\nexport default publicWidget.registry.websiteEventTrackProposalFormTags;\n", "/** @odoo-module **/\n\nimport { debounce } from \"@web/core/utils/timing\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { Component } from \"@odoo/owl\";\n\npublicWidget.registry.websiteEventTrackReminder = publicWidget.Widget.extend({\n    selector: '.o_wetrack_js_reminder',\n    events: {\n        'click': '_onReminderToggleClick',\n    },\n\n    /**\n     * @override\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n        this._onReminderToggleClick = debounce(this._onReminderToggleClick, 500, true);\n        this.notification = this.bindService(\"notification\");\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //-------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onReminderToggleClick: function (ev) {\n        ev.stopPropagation();\n        ev.preventDefault();\n        var self = this;\n        var $trackLink = $(ev.currentTarget).find('i');\n\n        if (this.reminderOn === undefined) {\n            this.reminderOn = $trackLink.data('reminderOn');\n        }\n\n        var reminderOnValue = !this.reminderOn;\n\n        rpc('/event/track/toggle_reminder', {\n            track_id: $trackLink.data('trackId'),\n            set_reminder_on: reminderOnValue,\n        }).then(function (result) {\n            if (result.error && result.error === 'ignored') {\n                self.notification.add(_t('Talk already in your Favorites'), {\n                    type: 'info',\n                    title: _t('Error'),\n                });\n            } else {\n                self.reminderOn = reminderOnValue;\n                var reminderText = self.reminderOn ? _t('Favorite On') : _t('Set Favorite');\n                self.$('.o_wetrack_js_reminder_text').text(reminderText);\n                self._updateDisplay();\n                var message = self.reminderOn ? _t('Talk added to your Favorites') : _t('Talk removed from your Favorites');\n                self.notification.add(message, {\n                    type: 'info',\n                });\n                if (self.reminderOn) {\n                    Component.env.bus.trigger('open_notification_request', [\n                        'add_track_to_favorite',\n                        {\n                            title: _t('Allow push notifications?'),\n                            body: _t('You have to enable push notifications to get reminders for your favorite tracks.'),\n                            delay: 0\n                        },\n                    ]);\n                }\n            }\n        });\n    },\n\n    _updateDisplay: function () {\n        var $trackLink = this.$el.find('i');\n        if (this.reminderOn) {\n            $trackLink.addClass('fa-bell').removeClass('fa-bell-o');\n            $trackLink.attr('title', _t('Favorite On'));\n        } else {\n            $trackLink.addClass('fa-bell-o').removeClass('fa-bell');\n            $trackLink.attr('title', _t('Set Favorite'));\n        }\n    },\n\n});\n\nexport default publicWidget.registry.websiteEventTrackReminder;\n", "/** @odoo-module **/\n\nimport { formatDuration } from \"@web/core/l10n/dates\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { _t } from \"@web/core/l10n/translation\";\nconst { DateTime } = luxon;\n\n/*\n * Simple implementation of a timer widget that uses a \"time to live\" configuration\n * value to countdown seconds on a target element.\n * Will be used to visually countdown the time before a talk starts.\n * When the timer reaches 0, the element destroys itself.\n */\npublicWidget.registry.websiteEventTrackTimer = publicWidget.Widget.extend({\n\n    selector: '.o_we_track_timer',\n    events: {\n        'click .close': '_onCloseClick',\n    },\n\n    /**\n     * @override\n     */\n    start: function () {\n        return this._super.apply(this, arguments).then(() => {\n            let timeToLive = this.$el.data('time-to-live');\n            let deadline = DateTime.now().plus({ seconds: timeToLive });\n            let remainingMs = deadline.diff(DateTime.now()).as(\"milliseconds\");\n            if (remainingMs > 0) {\n                this._updateTimerDisplay(remainingMs);\n                this.$el.removeClass('d-none');\n                this.deadline = deadline;\n                this.timer = setInterval(this._refreshTimer.bind(this), 1000);\n            } else {\n                this.destroy();\n            }\n        });\n    },\n\n    /**\n     * @override\n     */\n    destroy: function () {\n        this.$el.parent().remove();\n        clearInterval(this.timer);\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    _onCloseClick: function () {\n        this.destroy();\n    },\n\n    /**\n     * The function will trigger an update if the timer has not yet expired.\n     * Otherwise, the component will be destroyed.\n     */\n    _refreshTimer: function () {\n        let remainingMs = this.deadline.diffNow().as(\"milliseconds\");\n        if (remainingMs > 0) {\n            this._updateTimerDisplay(remainingMs);\n        } else {\n            this.destroy();\n        }\n    },\n\n    /**\n     * The function will have the responsibility to update the text indicating\n     * the time remaining before the counter expires. The function will use\n     * Luxon to transform the remaining time in more a human friendly format\n     * Example: \"in 32 minutes\", \"in 17 hours\", etc.\n     * @param {integer} remainingMs - Time remaining before the counter expires (in ms).\n     */\n    _updateTimerDisplay: function (remainingMs) {\n        let $timerTextEl = this.$el.find('span');\n        const humanDuration = formatDuration(remainingMs / 1000, true);\n        const str = _t(\"in %s\", humanDuration);\n        if (str !== $timerTextEl.text()) {\n            $timerTextEl.text(str);\n        }\n    },\n});\n\nexport default publicWidget.registry.websiteEventTrackTimer;\n", "/** @odoo-module **/\n\n    /*\n     * The \"deferredPrompt\" Promise will resolve only if the \"beforeinstallprompt\" event\n     * has been triggered. It allows to register this listener as soon as possible\n     * to avoid missed-events (as the browser can trigger it very early in the page lifecycle).\n     */\n    var deferredPrompt = new Promise(function (resolve, reject) {\n        if (\"serviceWorker\" in navigator) {\n            window.addEventListener(\"beforeinstallprompt\", function (ev) {\n                ev.preventDefault();\n                resolve(ev);\n            });\n        } else {\n            console.log(\"ServiceWorker not supported\");\n        }\n    });\n\n    import publicWidget from \"@web/legacy/js/public/public_widget\";\n    import { utils as uiUtils } from \"@web/core/ui/ui_service\";\n\n    var PWAInstallBanner = publicWidget.Widget.extend({\n        template: \"pwa_install_banner\",\n        events: {\n            \"click .o_btn_install\": \"_onClickInstall\",\n            \"click .o_btn_close\": \"_onClickClose\",\n        },\n\n        /**\n         * @private\n         */\n        _onClickClose: function () {\n            this.trigger_up(\"prompt_close_bar\");\n        },\n\n        /**\n         * @private\n         */\n        _onClickInstall: function () {\n            this.trigger_up(\"prompt_install\");\n        },\n    });\n\n    publicWidget.registry.WebsiteEventPWAWidget = publicWidget.Widget.extend({\n        selector: \"#wrapwrap.event\",\n        custom_events: {\n            prompt_install: \"_onPromptInstall\",\n            prompt_close_bar: \"_onPromptCloseBar\",\n        },\n\n        /**\n         *\n         * @override\n         */\n        start: function () {\n            var self = this;\n            return this._super.apply(this, arguments)\n                .then(this._registerServiceWorker.bind(this))\n                .then(function () {\n                    // Don't wait for the prompt's Promise as it may never resolve.\n                    deferredPrompt.then(self._showInstallBanner.bind(self));\n                })\n                .then(this._prefetch.bind(this));\n        },\n\n        /**\n         *\n         * @override\n         */\n        destroy: function () {\n            this._super.apply(this, arguments);\n        },\n\n        //--------------------------------------------------------------------------\n        // Private\n        //--------------------------------------------------------------------------\n\n        /**\n         * Returns the PWA's scope\n         *\n         * Note: this method performs a matching to handle URLs with the language prefix.\n         *       Typically this prefix is in the form of \"en\" or \"en_US\" but it can also be\n         *       any string using the customization options in the Website's settings.\n         * @private\n         * @returns {String}\n         */\n        _getScope: function () {\n            var matches = window.location.pathname.match(/^(\\/(?:event|[^/]+\\/event))\\/?/);\n            if (matches && matches[1]) {\n                return matches[1];\n            }\n            return \"/event\";\n        },\n\n        /**\n         * @private\n         */\n        _hideInstallBanner: function () {\n            this.installBanner ? this.installBanner.destroy() : undefined;\n            $(\".o_livechat_button\").css(\"bottom\", \"0\");\n        },\n\n        /**\n         * Parse the current page for first-level children pages and ask the ServiceWorker\n         * to already fetch them to populate the cache.\n         * @private\n         */\n        _prefetch: function () {\n            if (!(\"serviceWorker\" in navigator)) {\n                return;\n            }\n            var assetsUrls = Array.from(document.querySelectorAll('link[rel=\"stylesheet\"], script[src]')).map(function (el) {\n                return el.href || el.src;\n            });\n            navigator.serviceWorker.ready.then(function (registration) {\n                registration.active.postMessage({\n                    action: \"prefetch-assets\",\n                    urls: assetsUrls,\n                });\n            }).catch(function (error) {\n                console.error(\"Service worker ready failed, error:\", error);\n            });\n            var currentPageUrl = window.location.href;\n            var childrenPagesUrls = Array.from(document.querySelectorAll('a[href^=\"' + this._getScope() + '/\"]')).map(function (el) {\n                return el.href;\n            });\n            navigator.serviceWorker.ready.then(function (registration) {\n                registration.active.postMessage({\n                    action: \"prefetch-pages\",\n                    urls: childrenPagesUrls.concat(currentPageUrl),\n                });\n            }).catch(function (error) {\n                console.error(\"Service worker ready failed, error:\", error);\n            });\n        },\n\n        /**\n         *\n         * @private\n         */\n        _registerServiceWorker: function () {\n            if (!(\"serviceWorker\" in navigator)) {\n                return;\n            }\n            var scope = this._getScope();\n            return navigator.serviceWorker\n                .register(scope + \"/service-worker.js\", { scope: scope })\n                .catch(function (error) {\n                    console.error(\"Service worker registration failed, error:\", error);\n                });\n        },\n\n        /**\n         * @private\n         */\n        _showInstallBanner: function () {\n            if (!uiUtils.isSmall()) {\n                return;\n            }\n            var self = this;\n            this.installBanner = new PWAInstallBanner(this);\n            this.installBanner.appendTo(this.$el).then(function () {\n                // If Livechat available, It should be placed above the PWA banner.\n                var height = self.$(\".o_pwa_install_banner\").outerHeight(true);\n                $(\".o_livechat_button\").css(\"bottom\", height + \"px\");\n            });\n        },\n\n        //--------------------------------------------------------------------------\n        // Handlers\n        //--------------------------------------------------------------------------\n\n        /**\n         * @private\n         * @param ev {Event}\n         */\n        _onPromptCloseBar: function (ev) {\n            ev.stopPropagation();\n            this._hideInstallBanner();\n        },\n        /**\n         * @private\n         * @param ev {Event}\n         */\n        _onPromptInstall: function (ev) {\n            ev.stopPropagation();\n            this._hideInstallBanner();\n            deferredPrompt.then(function (prompt) {\n                    prompt.prompt();\n                    prompt.userChoice.then(function (choiceResult) {\n                        if (choiceResult.outcome === \"accepted\") {\n                            console.log(\"User accepted the install prompt\");\n                        } else {\n                            console.log(\"User dismissed the install prompt\");\n                        }\n                    });\n                });\n        },\n    });\n\n    export default {\n        PWAInstallBanner: PWAInstallBanner,\n        WebsiteEventPWAWidget: publicWidget.registry.WebsiteEventPWAWidget,\n    };\n", "//     idb-keyval.js 3.2.0\n//     https://github.com/jakearchibald/idb-keyval\n//     Copyright 2016, Jake Archibald\n//     Licensed under the Apache License, Version 2.0\n\nvar idbKeyval = (function (exports) {\n    'use strict';\n    \n    class Store {\n        constructor(dbName = 'keyval-store', storeName = 'keyval') {\n            this.storeName = storeName;\n            this._dbp = new Promise((resolve, reject) => {\n                const openreq = indexedDB.open(dbName, 1);\n                openreq.onerror = () => reject(openreq.error);\n                openreq.onsuccess = () => resolve(openreq.result);\n                // First time setup: create an empty object store\n                openreq.onupgradeneeded = () => {\n                    openreq.result.createObjectStore(storeName);\n                };\n            });\n        }\n        _withIDBStore(type, callback) {\n            return this._dbp.then(db => new Promise((resolve, reject) => {\n                const transaction = db.transaction(this.storeName, type);\n                transaction.oncomplete = () => resolve();\n                transaction.onabort = transaction.onerror = () => reject(transaction.error);\n                callback(transaction.objectStore(this.storeName));\n            }));\n        }\n    }\n    let store;\n    function getDefaultStore() {\n        if (!store)\n            store = new Store();\n        return store;\n    }\n    function get(key, store = getDefaultStore()) {\n        let req;\n        return store._withIDBStore('readonly', store => {\n            req = store.get(key);\n        }).then(() => req.result);\n    }\n    function set(key, value, store = getDefaultStore()) {\n        return store._withIDBStore('readwrite', store => {\n            store.put(value, key);\n        });\n    }\n    function del(key, store = getDefaultStore()) {\n        return store._withIDBStore('readwrite', store => {\n            store.delete(key);\n        });\n    }\n    function clear(store = getDefaultStore()) {\n        return store._withIDBStore('readwrite', store => {\n            store.clear();\n        });\n    }\n    function keys(store = getDefaultStore()) {\n        const keys = [];\n        return store._withIDBStore('readonly', store => {\n            // This would be store.getAllKeys(), but it isn't supported by Edge or Safari.\n            // And openKeyCursor isn't supported by Safari.\n            (store.openKeyCursor || store.openCursor).call(store).onsuccess = function () {\n                if (!this.result)\n                    return;\n                keys.push(this.result.key);\n                this.result.continue();\n            };\n        }).then(() => keys);\n    }\n    \n    exports.Store = Store;\n    exports.get = get;\n    exports.set = set;\n    exports.del = del;\n    exports.clear = clear;\n    exports.keys = keys;\n    \n    return exports;\n    \n    }({}));\n", "/** @odoo-module **/\n\nimport PlanningView from '@planning/js/planning_calendar_front';\n\nPlanningView.include({\n    // override popup of calendar\n    eventFunction: function (calEvent) {\n        this._super.apply(this, arguments);\n        const $saleLine = $(\"#sale_line\");\n        if (calEvent.event.extendedProps.sale_line) {\n            $saleLine.text(calEvent.event.extendedProps.sale_line);\n            $saleLine.css(\"display\", \"\");\n            $saleLine.prev().css(\"display\", \"\");\n        } else {\n            $saleLine.css(\"display\", \"none\");\n            $saleLine.prev().css(\"display\", \"none\");\n        }\n    },\n});\n", "$(function() {\nif (odoo && odoo.__DEBUG__ === undefined) {\n    // skip if >= 17.0\n    return;\n}\n\nconst resend_activation_email_modal_frontend_deps = [\n    'web.ajax',\n    'web.core',\n    'web.session',\n    'web.Widget',\n];\nodoo.define('saas_trial.resend_activation_email_modal_frontend',\n            resend_activation_email_modal_frontend_deps,\n            function (require) {\n    'use strict';\n    var core = require('web.core');\n    var _t = core._t;\n    var ajax = require('web.ajax');\n    var session = require('web.session');\n    var Widget = require('web.Widget');\n\n    var ResendActivationEmailModal = Widget.extend({\n        template: 'SaasTrial.ResendEmailModal',\n        events: {\n            'submit form': '_send_email',\n        },\n        init: function(parent, usedEmail, email, expire_time) {\n            this._super(parent);\n            this.usedEmail = usedEmail;\n            this.email = email;\n            this.isAdmin = session.is_admin || session.is_system;\n            this.expire_time = expire_time;\n            this.time_left = moment().utc().from(expire_time, true);\n            this._refresh_time_left();\n        },\n        start: function() {\n            this.$el.find('.modal').modal('show');\n        },\n        _send_email: function(e) {\n            e.preventDefault();\n            e.stopPropagation();\n            var self = this;\n            var email = $(e.target).find('input[name=\"email\"]').val();\n            var send_btn = $(e.target).find('input[type=\"submit\"]');\n            send_btn.val(_t('Sending...'));\n            send_btn.prop('disabled', true);\n            var params = {\n                'activation_email': email,\n            };\n            ajax.jsonRpc('/saas_worker/send_activation_email', 'call', params).then(function(result) {\n                if(result.success) {\n                    self.$el.find('.modal-body').text(result.success);\n                }\n                else {\n                    self.$el.find('.modal-body').text(result.error);\n                }\n            });\n        },\n        _refresh_time_left: function() {\n            var self = this;\n            setTimeout(function() {\n                if (self.isDestroyed()) {\n                    return;\n                }\n                self.time_left = moment().utc().from(self.expire_time, true);\n                self._refresh_time_left();\n                self.$el.find('.oe_time_left').text(self.time_left);\n            }, 60000);\n        },\n    });\n    return ResendActivationEmailModal;\n});\nvar deps = [\n    'saas_trial.resend_activation_email_modal_frontend',\n    'saas_trial.trial_info_frontend',\n    'web.core',\n    'web.session',\n    'web.Widget',\n];\n// >= saas-11: web_enterprise.Menu was introduced in saas-11\n// if backend\nif (Object.prototype.hasOwnProperty.call(odoo.__DEBUG__.services, 'web_enterprise.Menu')) {\n    deps.push('web.SystrayMenu');\n}\nodoo.define('saas_trial.db_expiration_tag_frontend', deps, function (require) {\n    'use strict';\n    var core = require('web.core');\n    var Widget = require('web.Widget');\n    var ResendActivationEmailModal = require('saas_trial.resend_activation_email_modal_frontend');\n    var session = require('web.session');\n    const get_trial_info = require('saas_trial.trial_info_frontend');\n    var ExpirationTag = Widget.extend({\n        xmlDependencies: ['/saas_trial/static/xml/trial.xml'],\n        template: 'saas_trial.db_expiration_tag',\n        events: {\n            'click a': '_open_modal',\n        },\n        init: function(parent) {\n            this._super(parent);\n            core.bus.on('db_activation_requested', this, this._on_db_activation_requested);\n        },\n        willStart: async function() {\n            await this._super.apply(this, arguments);\n\n            const trial_info = await get_trial_info();\n            this.usedEmail = trial_info.activation_email;\n            this.email = trial_info.user_email;\n            this.expire_time = moment.utc(trial_info.expiry_oe || trial_info.expiry);\n            this.visible = trial_info.status === 'to_activate' && session.is_admin;\n        },\n        _open_modal: function(e) {\n            if (e) {\n                e.preventDefault();\n            }\n            var modal = new ResendActivationEmailModal(this, this.usedEmail, this.email, this.expire_time);\n            modal.appendTo($('body'));\n        },\n        _on_db_activation_requested: function() {\n            this._open_modal(undefined);\n        },\n    });\n    if (Object.prototype.hasOwnProperty.call(odoo.__DEBUG__.services, 'web_enterprise.Menu')) {\n        var SystrayMenu = require('web.SystrayMenu');\n        SystrayMenu.Items.push(ExpirationTag);\n    }\n    return ExpirationTag;\n});\nconst trial_info_frontend_deps = [\n    'web.session',\n];\nodoo.define('saas_trial.trial_info_frontend', trial_info_frontend_deps, function (require) {\n    'use strict';\n    var session = require('web.session');\n    let trial_info_promise = null;\n    async function get_trial_info() {\n        if (trial_info_promise === null) {\n            trial_info_promise = session.rpc('/saas_worker/trial_info', {});\n        }\n        return await trial_info_promise;\n    }\n    return get_trial_info;\n});\n});\n", "$(function() {\n\n    import('/saas_website/static/src/lib/owl_version.js').then((ov) => {\n        const o_ver = ov.owl_version;\n        const o_version = o_ver();\n\n        // Backend equivalent is used instead in owl version >= 2.2.5 (i.e for odoo >= 16.0)\n        if (!(o_version.check(2, 2, 5))) {\n            // >= saas-12.3: website.root was introduced in saas-12.3\n            if (Object.prototype.hasOwnProperty.call(odoo.__DEBUG__.services, 'website.root')) {\n                const db_expiration_tag_dep = (\n                    // using odoo.session_info !== undefined as a loose check for <15.0\n                    'saas_trial.db_expiration_tag_' + (odoo.session_info !== undefined ? 'legacy' : 'frontend')\n                );\n                const db_activation_website_deps = [\n                    db_expiration_tag_dep,\n                    'website.root',\n                ];\n                odoo.define('saas_website.db_activation_website', db_activation_website_deps, function (require) {\n                    'use strict';\n                    var ExpirationTag = require(db_expiration_tag_dep);\n\n                    // useless for public visitor that will not see it\n                    if (!$('.o_menu_systray').length) {\n                        return;\n                    }\n                    // The systray was refactored in saas-14.5, meaning styles would break\n                    // if we reuse the same template.\n                    if ($('div.o_menu_systray>div').length > 0) {\n                        ExpirationTag.include({template: 'saas_trial.db_expiration_tag_website_15'});\n                    }\n                    var WebsiteRoot = require('website.root').WebsiteRoot;\n                    WebsiteRoot.include({\n                        xmlDependencies: (WebsiteRoot.prototype.xmlDependencies || [])\n                            .concat(['/saas_trial/static/xml/trial.xml']),\n                        start: function() {\n                            return this._super.apply(this, arguments).then(function() {\n                                // website: no 'web.SystrayMenu', insert directly into the DOM.\n                                var tag = new ExpirationTag(this);\n                                tag.prependTo($('.o_menu_systray'));\n                            });\n                        },\n                    });\n                });\n            }\n        }\n    });\n});\n", "(function () {\n    /* HELPERS\n    ---------------------------------*/\n    function getXpathNodes(xpathResult) {\n        const nodes = [];\n        let res;\n        while ((res = xpathResult.iterateNext())) {\n            nodes.push(res);\n        }\n        return nodes;\n    }\n\n    function getNodesFromXpath(xpath, xml) {\n        const owner = \"evaluate\" in xml ? xml : xml.ownerDocument;\n        const xpathResult = owner.evaluate(xpath, xml, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);\n        return getXpathNodes(xpathResult);\n    }\n\n    function extendTemplate(templateElem, owlXML, extensions = {}) {\n        var s = new XMLSerializer();\n        const parser = new DOMParser();\n\n        // Make a shallow copy\n        templateStr = s.serializeToString(templateElem);\n        templateXml = parser.parseFromString(templateStr, \"text/xml\");\n\n        let hasExt = false;\n        for (const [xpath, fn] of Object.entries(extensions)) {\n            hasExt = true;\n            const nodes = getNodesFromXpath(xpath, templateXml);\n            if (nodes.length >= 1) {\n                fn(...nodes);\n            } else {\n                throw new Error(`xpath: ${xpath} did not return anything.`);\n            }\n        }\n\n        if (!hasExt) {\n            return templateId;\n        }\n        return owlXML`${s.serializeToString(templateXml)}`;\n    }\n\n    /* OVERRIDES\n    ---------------------------------*/\n    import('/saas_website/static/src/lib/owl_version.js').then((ov) => {\n        const o_ver = ov.owl_version;\n        const o_version = o_ver();\n\n        // OWL 2.0.2+ version must be in backend bundle since v16.0\n        if (o_version.check(1, 4, 10) && !o_version.check(2, 0, 2)) {\n            // >= 15.0\n            deps = {\n                FeaturesSelectionScreen: \"@website/components/configurator/configurator\",\n                useService: '@web/core/utils/hooks',\n                patch: '@web/core/utils/patch',\n                _t: '@web/core/l10n/translation',\n            };\n\n            // >= saas-15.2, owl.tags.xml -> owl.xml\n            let owlXML = owl.xml ? owl.xml : owl.tags.xml;\n\n            odoo.define('saas_website.WebsiteFeatureSelectorOwl1', Object.values(deps), function (require) {\n                const { FeaturesSelectionScreen } = require(deps.FeaturesSelectionScreen);\n                const { useService } = require(deps.useService);\n                const { patch } = require(deps.patch);\n                const { _t } = require(deps._t);\n\n                patch(FeaturesSelectionScreen.prototype, 'saas_website_configurator_feature_selection', {\n                    setup() {\n                        this._super(...arguments);\n                        this.rpc = useService('rpc');\n                    },\n\n                    show1AppFreeWarning() {\n                        return (\n                            (Object.values(this.state.features).filter(\n                                f => f.is_always_paid && f.selected\n                            ).length > 0) ||\n                            (Object.values(this.state.features).filter(\n                                f => f.is_1appmax && f.selected\n                            ).length > 1)\n                        );\n                    },\n                    async willStart() {\n                        /* We can't use the saas owl service because the env used by the v15.0\n                        configurator because it relies on the legacy one (v14.0), and the saas service\n                        is loaded in the non-legacy (v15.0) */\n                        this.saas = await this.rpc({route: '/saas_worker/trial_info'});\n\n                        /* We can't use xml heritage here because the template used by by the v15.0\n                        configurator is loaded manually. The least invasive solution is to manually\n                        apply the heritage with JS at willStart instead.*/\n                        const qweb = this.env.qweb;\n                        const configuratorTmpl = qweb.templates['website.Configurator.FeatureSelection'].elem;\n                        this.configuratorTmplExtended = extendTemplate(configuratorTmpl, owl.tags.xml, {\n                            \"//div[@class='text-right border-top pt-2']\": (node) => {\n                                node.classList.add('d-flex', 'justify-content-between');\n\n                                const tIf = node.ownerDocument.createElement('p');\n                                tIf.innerHTML = _t('Paid Plan');\n                                tIf.setAttribute(\"t-if\", \"saas.mode ==='paid'\");\n\n                                const tElif = node.ownerDocument.createElement('p');\n                                const tElifInnerTxt = _t('Standard Plan (15 days trial)');\n                                tElif.innerHTML = `<i class=\"fa fa-exclamation-triangle\"/> ${tElifInnerTxt}`;\n                                tElif.setAttribute(\"t-elif\", \"show1AppFreeWarning()\");\n                                tElif.classList.add(\"text-danger\");\n\n                                const tElse = node.ownerDocument.createElement('p');\n                                tElse.innerHTML = _t('Free Plan');\n                                tElse.setAttribute(\"t-else\", \"\");\n\n                                node.prepend(tElse);\n                                node.prepend(tElif);\n                                node.prepend(tIf);\n                            }\n                        });\n                    }\n\n\n                });\n\n                /* We still have to use a copy of the parent template (heritage mode primary) to ensure\n                that owl will not use a cached rendering if the template before the modification from\n                willStart are applied. */\n                patch(FeaturesSelectionScreen, 'saas_website_configurator_feature_selection_template', {\n                    template: owlXML`<t t-call=\"{{configuratorTmplExtended}}\"/>`\n                });\n\n            });\n        }\n    });\n})();\n", "/** @odoo-module **/\n\nimport CourseJoin from \"@website_slides/js/slides_course_join\";\nimport wUtils from \"@website/js/utils\";\n\nconst CourseJoinWidget = CourseJoin.courseJoinWidget;\n\nCourseJoinWidget.include({\n    init: function (parent, options) {\n        this._super.apply(this, arguments);\n        this.productId = options.channel.productId || false;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * When the user joins the course, if it's set as \"on payment\" and the user is logged in,\n     * we redirect to the shop page for this course.\n     *\n     * @param {MouseEvent} ev\n     * @override\n     * @private\n     */\n    _onClickJoin: function (ev) {\n        ev.preventDefault();\n\n        if (this.channel.channelEnroll === 'payment' && !this.publicUser) {\n            const self = this;\n            this.beforeJoin().then(function () {\n                wUtils.sendRequest('/shop/cart/update', {\n                    product_id: self.productId,\n                    express: 1,\n                });\n            });\n        } else {\n            this._super.apply(this, arguments);\n        }\n    },\n});\n\nexport default CourseJoinWidget;\n", "/** @odoo-module **/\n\nimport {websiteSlidesQuizNoFullscreen} from \"@website_slides/js/slides_course_quiz\";\n\nwebsiteSlidesQuizNoFullscreen.include({\n    _extractChannelData: function (slideData) {\n        return Object.assign({}, this._super.apply(this, arguments), {\n            productId: slideData.productId,\n            enroll: slideData.enroll,\n            currencyName: slideData.currencyName,\n            currencySymbol: slideData.currencySymbol,\n            price: slideData.price,\n            hasDiscountedPrice: slideData.hasDiscountedPrice\n        });\n    }\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { user } from \"@web/core/user\";\n\npublicWidget.registry.appointmentSaleConfirmation = publicWidget.Widget.extend({\n    selector: '.o_wappointment_sale_confirmation_card',\n\n    /**\n     * Store in local storage the appointment booked for the appointment type.\n     * This value is used later to display information on the upcoming appointment\n     * if an appointment is already taken. If the user is logged don't store anything\n     * as everything is computed by the /appointment/get_upcoming_appointments route.\n     * @override\n     */\n    start: function() {\n        return this._super(...arguments).then(() => { \n            if (user.userId) {\n                return;\n            }\n            const eventAccessToken = this.el.dataset.eventAccessToken;\n            const allAppointmentsToken = JSON.parse(localStorage.getItem('appointment.upcoming_events_access_token')) || [];\n            if (eventAccessToken && !allAppointmentsToken.includes(eventAccessToken)) {\n                allAppointmentsToken.push(eventAccessToken);\n                localStorage.setItem('appointment.upcoming_events_access_token', JSON.stringify(allAppointmentsToken));\n            }\n        });\n    },\n});\n", "/** @odoo-module **/\n\nimport \"@website/snippets/s_website_form/000\";  // force deps\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { session } from \"@web/session\";\npublicWidget.registry.s_website_form.include({\n    /**\n     * @override\n    */\n    start: function () {\n        const res = this._super(...arguments);\n        this.cleanTurnstile();\n        if (!this.isEditable && !this.el.querySelector(\".s_turnstile\") && session.turnstile_site_key) {\n            this.uniq = uniqueId(\"turnstile_\");\n            this.el.classList.add(this.uniq);\n\n            const mode = new URLSearchParams(window.location.search).get(\"cf\") == \"show\" ? \"always\" : \"interaction-only\";\n            const turnstileEl = document.createElement(\"div\");\n            turnstileEl.className = \"s_turnstile cf-turnstile float-end\";\n            turnstileEl.dataset.action = \"website_form\";\n            turnstileEl.dataset.appearance = mode;\n            turnstileEl.dataset.responseFieldName = \"turnstile_captcha\";\n            turnstileEl.dataset.sitekey = session.turnstile_site_key;\n            turnstileEl.dataset.errorCallback = \"throwTurnstileError\";\n            turnstileEl.dataset.beforeInteractiveCallback = \"turnstileBeforeInteractive\";\n            turnstileEl.dataset.afterInteractiveCallback = \"turnstileAfterInteractive\";\n\n            const script1El = document.createElement(\"script\");\n            script1El.className = \"s_turnstile\";\n            script1El.textContent = `\n                // Rethrow the error, or we only will catch a \"Script error\" without any info\n                // because of the script api.js originating from a different domain.\n                function throwTurnstileError(code) {\n                    const error = new Error(\"Turnstile Error\");\n                    error.code = code;\n                    throw error;\n                }\n                function turnstileBeforeInteractive() {\n                    const btnEl = document.querySelector('.${this.uniq} .s_website_form_send,.${this.uniq} .o_website_form_send');\n                    if (btnEl && !btnEl.classList.contains('disabled')) {\n                        btnEl.classList.add('disabled', 'cf_form_disabled');\n                    }\n                }\n                function turnstileAfterInteractive() {\n                    const btnEl = document.querySelector('.${this.uniq} .s_website_form_send,.${this.uniq} .o_website_form_send');\n                    if (btnEl && btnEl.classList.contains('cf_form_disabled')) {\n                        btnEl.classList.remove('disabled', 'cf_form_disabled');\n                    }\n                }\n            `;\n\n            const script2El = document.createElement(\"script\");\n            script2El.className = \"s_turnstile\";\n            script2El.src = \"https://challenges.cloudflare.com/turnstile/v0/api.js\";\n\n            const formSendEl = this.el.querySelector(\".s_website_form_send, .o_website_form_send\");\n            formSendEl.parentNode.insertBefore(turnstileEl, formSendEl.nextSibling);\n            formSendEl.parentNode.insertBefore(script1El, formSendEl.nextSibling);\n            formSendEl.parentNode.insertBefore(script2El, formSendEl.nextSibling);\n        }\n        return res;\n    },\n\n    /**\n     * Remove potential existing loaded script/token\n    */\n    cleanTurnstile: function () {\n        const turnstileEls = this.el.querySelectorAll(\".s_turnstile\");\n        turnstileEls.forEach(element => element.remove());\n    },\n\n    /**\n     * @override\n     * Discard all library changes to reset the state of the Html.\n    */\n    destroy: function () {\n        this.cleanTurnstile();\n        this._super(...arguments);\n    },\n});\n", "/** @odoo-module */\n\nimport { ErrorDialog } from \"@web/core/errors/error_dialogs\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nfunction turnstileErrorHandler(env, error) {\n    if (error.message.includes(\"Turnstile Error\")) {\n        env.services.dialog.add(ErrorDialog, {\n            name: _t(\"Cloudflare Turnstile Error\"),\n            traceback: _t(\n                `There was an error with Cloudflare Turnstile, the captcha system.\\n` +\n                `Please make sure your credentials for this service are properly set up.\\n` +\n                `The error code is: %s.\\n` +\n                `You can find more information about this error code here: https://developers.cloudflare.com/turnstile/reference/errors.`,\n                error.event.error.code\n            ),\n        });\n        return true;\n    }\n}\n\nregistry.category(\"error_handlers\").add(\"turnstile_error_handler\", turnstileErrorHandler);\n", "/** @odoo-module **/\n\nimport { renderToElement, renderToFragment } from \"@web/core/utils/render\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { post } from \"@web/core/network/http_service\";\nimport { redirect } from \"@web/core/utils/urls\";\n\npublicWidget.registry.boothRegistration = publicWidget.Widget.extend({\n    selector: '.o_wbooth_registration',\n    events: {\n        'change input[name=\"booth_category_id\"]': '_onChangeBoothType',\n        'change .form-check > input[type=\"checkbox\"]': '_onChangeBooth',\n        'click .o_wbooth_registration_submit': '_onSubmitBoothSelectionClick',\n        'click .o_wbooth_registration_confirm': '_onConfirmRegistrationClick',\n    },\n\n    start() {\n        this.eventId = parseInt(this.el.dataset.eventId);\n        this.activeBoothCategoryId = false;\n        this.boothCache = {};\n        this.boothsFirstRendering = true;\n        this.selectedBoothIds = [];\n        return this._super.apply(this, arguments).then(() => {\n            this.selectedBoothCategory = this.el.querySelector('input[name=\"booth_category_id\"]:checked');\n            if (this.selectedBoothCategory) {\n                this.selectedBoothIds = this.el.querySelector('.o_wbooth_booths').dataset.selectedBoothIds.split(',').map(Number);\n                this.activeBoothCategoryId = this.selectedBoothCategory.value;\n                this._fetchBoothsAndUpdateUI();\n            }\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _check_booths_availability(eventBoothIds) {\n        const self = this;\n        return rpc(\"/event/booth/check_availability\", {\n            event_booth_ids: eventBoothIds,\n        }).then(function (result) {\n            if (result.unavailable_booths.length) {\n                for (const el of self.el.querySelectorAll(\"input[name='event_booth_ids']\")) {\n                    if (result.unavailable_booths.includes(parseInt(el.value))) {\n                        el.closest(\".form-check\").classList.add(\"text-danger\");\n                    }\n                }\n                self.el\n                    .querySelector(\".o_wbooth_unavailable_booth_alert\")\n                    .classList.remove(\"d-none\");\n                return Promise.resolve(false);\n            }\n            return Promise.resolve(true);\n        })\n    },\n\n    _countSelectedBooths() {\n        return this.el.querySelectorAll(\".form-check > input[type='checkbox']:checked\").length;\n    },\n\n    _fillBooths() {\n        const boothsElem = this.el.querySelector('.o_wbooth_booths');\n        boothsElem.replaceChildren(renderToFragment('event_booth_checkbox_list', {\n            'event_booth_ids': this.boothCache[this.activeBoothCategoryId],\n            'selected_booth_ids': this.boothsFirstRendering ? this.selectedBoothIds : [],\n        }));\n\n        this.boothsFirstRendering = false;\n    },\n\n    /**\n     * Check if the confirmation form is valid by testing each of its inputs\n     *\n     * @private\n     * @param formEl\n     * @return {boolean} - true if no errors else false\n     */\n    _isConfirmationFormValid(formEl) {\n        const formErrors = [];\n        for (const el of formEl.querySelectorAll(\".form-control\")) {\n            el.classList.remove(\"is-invalid\");\n            if (!el.checkValidity()) {\n                el.classList.add(\"is-invalid\");\n                formErrors.push('invalidFormInputs');\n            }\n        }\n\n        this._updateErrorDisplay(formErrors);\n        return formErrors.length === 0;\n    },\n\n    _showBoothCategoryDescription() {\n        for (const el of this.el.querySelectorAll(\".o_wbooth_booth_category_description\")) {\n            el.classList.add(\"d-none\");\n        }\n        this.el\n            .querySelector(\"#o_wbooth_booth_description_\" + this.activeBoothCategoryId)\n            .classList.remove(\"d-none\");\n    },\n\n    /**\n     * Display the errors with a custom message when confirming\n     * the registration if there is any.\n     *\n     * @private\n     * @param errors\n     */\n    _updateErrorDisplay(errors) {\n        this.el\n            .querySelector(\".o_wbooth_registration_error_section\")\n            .classList.toggle(\"d-none\", !errors.length);\n\n        let errorMessages = [];\n        const errorMessageEl = this.el.querySelector(\".o_wbooth_registration_error_message\");\n\n        if (errors.includes('invalidFormInputs')) {\n            errorMessages.push(_t(\"Please fill out the form correctly.\"));\n        }\n\n        if (errors.includes('boothError')) {\n            errorMessages.push(_t(\"Booth registration failed.\"));\n        }\n\n        if (errors.includes('boothCategoryError')) {\n            errorMessages.push(_t(\"The booth category doesn't exist.\"));\n        }\n\n        errorMessageEl.textContent = errorMessages.join(\" \");\n        errorMessageEl.dispatchEvent(new Event(\"change\"));\n    },\n\n    _updateUiAfterBoothCategoryChange() {\n        this._fillBooths();\n        this._showBoothCategoryDescription();\n        this._updateUiAfterBoothChange(this._countSelectedBooths());\n    },\n\n    _updateUiAfterBoothChange(boothCount) {\n        const buttonEl = this.el.querySelector(\"button.o_wbooth_registration_submit\");\n        if (buttonEl) {\n            buttonEl.disabled = !boothCount;\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    _onChangeBooth(ev) {\n        ev.currentTarget.closest(\".form-check\").classList.remove(\"text-danger\");\n        this._updateUiAfterBoothChange(this._countSelectedBooths());\n    },\n\n    _onChangeBoothType(ev) {\n        ev.preventDefault();\n        this.activeBoothCategoryId = parseInt(ev.currentTarget.value);\n        this._fetchBoothsAndUpdateUI();\n    },\n\n    /**\n     * Load all the booths related to the activeBoothCategoryId booth category and\n     * add them to a local dictionary to avoid making rpc each time the\n     * user change the booth category.\n     *\n     * Then the selection input will be filled with the fetched booth values.\n     *\n     * @private\n     */\n    _fetchBoothsAndUpdateUI() {\n        if (this.boothCache[this.activeBoothCategoryId] === undefined) {\n            var self = this;\n            rpc('/event/booth_category/get_available_booths', {\n                event_id: this.eventId,\n                booth_category_id: this.activeBoothCategoryId,\n            }).then(function (result) {\n                self.boothCache[self.activeBoothCategoryId] = result;\n                self._updateUiAfterBoothCategoryChange();\n            });\n        } else {\n            this._updateUiAfterBoothCategoryChange();\n        }\n    },\n\n    async _onSubmitBoothSelectionClick(ev) {\n        ev.preventDefault();\n        const formEl = this.el.querySelector(\".o_wbooth_registration_form\");\n        const eventBoothIds = [\n            ...this.el.querySelectorAll(\"input[name=event_booth_ids]:checked\"),\n        ].map((el) => parseInt(el.value));\n        if (await this._check_booths_availability(eventBoothIds)) {\n            formEl.submit();\n        }\n    },\n\n    /**\n     * Submit the confirmation form if no errors are present after validation.\n     *\n     * If the submission succeed, we replace the form with a success message template.\n     *\n     * @param ev\n     * @return {Promise<void>}\n     * @private\n     */\n    async _onConfirmRegistrationClick(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n\n        ev.currentTarget.classList.add(\"disabled\");\n        ev.currentTarget.disabled = true;\n\n        const formEl = this.el.querySelector(\"#o_wbooth_contact_details_form\");\n        if (this._isConfirmationFormValid(formEl)) {\n            const formData = new FormData(formEl);\n            const jsonResponse = await post(`/event/${encodeURIComponent(this.el.dataset.eventId)}/booth/confirm`, formData);\n            if (jsonResponse.success) {\n                this.el.querySelector('.o_wevent_booth_order_progress').remove();\n                const boothCategoryId = this.el.querySelector('input[name=booth_category_id]').value;\n                const boothRegistrationCompleteFormEl = renderToElement(\"event_booth_registration_complete\", {\n                    booth_category_id: boothCategoryId,\n                    event_id: this.eventId,\n                    event_name: jsonResponse.event_name,\n                    contact: jsonResponse.contact,\n                });\n                formEl.insertAdjacentElement(\"afterend\", boothRegistrationCompleteFormEl);\n                formEl.remove();\n            } else if (jsonResponse.redirect) {\n                redirect(jsonResponse.redirect);\n            } else if (jsonResponse.error) {\n                this._updateErrorDisplay(jsonResponse.error);\n            }\n        }\n\n        ev.currentTarget.classList.remove(\"disabled\");\n        ev.currentTarget.removeAttribute(\"disabled\");\n    },\n\n});\n\nexport default publicWidget.registry.boothRegistration;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { utils as uiUtils } from \"@web/core/ui/ui_service\";\nimport { rpc } from \"@web/core/network/rpc\";\n\npublicWidget.registry.ChatRoom = publicWidget.Widget.extend({\n    selector: '.o_wjitsi_room_widget',\n    events: {\n        'click .o_wjitsi_room_link': '_onChatRoomClick',\n    },\n\n    /**\n      * Manage the chat room (Jitsi), update the participant count...\n      *\n      * The widget takes some options\n      * - 'room-name', the name of the Jitsi room\n      * - 'chat-room-id', the ID of the `chat.room` record\n      * - 'auto-open', the chat room will be automatically opened when the page is loaded\n      * - 'check-full', check if the chat room is full before joining\n      * - 'attach-to', a JQuery selector of the element on which we will add the Jitsi\n      *                iframe. If nothing is specified, it will open a modal instead.\n      * - 'default-username': the username to use in the chat room\n      * - 'jitsi-server': the domain name of the Jitsi server to use\n      */\n    start: async function () {\n        await this._super.apply(this, arguments);\n        this.roomName = this.$el.data('room-name');\n        this.chatRoomId = parseInt(this.$el.data('chat-room-id'));\n        // automatically open the current room\n        this.autoOpen = parseInt(this.$el.data('auto-open') || 0);\n        // before joining, perform a RPC call to verify that the chat room is not full\n        this.checkFull = parseInt(this.$el.data('check-full') || 0);\n        // query selector of the element on which we attach the Jitsi iframe\n        // if not defined, the widget will pop in a modal instead\n        this.attachTo = this.$el.data('attach-to') || false;\n        // default username for jitsi\n        this.defaultUsername = this.$el.data('default-username') || false;\n\n        this.jitsiServer = this.$el.data('jitsi-server') || 'meet.jit.si';\n\n        this.maxCapacity = parseInt(this.$el.data('max-capacity')) || Infinity;\n\n        if (this.autoOpen) {\n            await this._onChatRoomClick();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n      * Click on a chat room to join it.\n      *\n      * @private\n      */\n    _onChatRoomClick: async function () {\n        if (this.checkFull) {\n            // maybe we didn't refresh the page for a while and so we might join a room\n            // which is full, so we perform a RPC call to verify that we can really join\n            let isChatRoomFull = await rpc('/jitsi/is_full', { room_name: this.roomName });\n\n            if (isChatRoomFull) {\n                window.location.reload();\n                return;\n            }\n        }\n\n        if (await this._openMobileApplication(this.roomName)) {\n            // we opened the mobile application\n            return;\n        }\n\n        await this._loadJisti();\n\n        if (this.attachTo) {\n            // attach the Jitsi iframe on the given parent node\n            let $parentNode = $(this.attachTo);\n            $parentNode.find(\"iframe\").trigger(\"empty\");\n            $parentNode.empty();\n\n            await this._joinJitsiRoom($parentNode);\n        } else {\n            // create a model and append the Jitsi iframe in it\n            let $jitsiModal = $(renderToElement('chat_room_modal', {}));\n            $(\"body\").append($jitsiModal);\n            $jitsiModal.modal('show');\n\n            let jitsiRoom = await this._joinJitsiRoom($jitsiModal.find('.modal-body'));\n\n            // close the modal when hanging up\n            jitsiRoom.addEventListener('videoConferenceLeft', async () => {\n                $('.o_wjitsi_room_modal').modal('hide');\n            });\n\n            // when the modal is closed, delete the Jitsi room object and clear the DOM\n            $jitsiModal.on('hidden.bs.modal', async () => {\n                jitsiRoom.dispose();\n                $(\".o_wjitsi_room_modal\").remove();\n            });\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n      * Jitsi do not provide an REST API to get the number of participant in a room.\n      * The only way to get the number of participant is to be in the room and to use\n      * the Javascript API. So, to update the participant count on the server side,\n      * the participant have to send the count in RPC...\n      *\n      * When leaving a room, the event \"participantLeft\" is called for the current user\n      * once per participant in the room (like if all other participants were leaving the\n      * room and then the current user himself).\n      *\n      * \"participantLeft\" is called only one time for the other participant who are still\n      * in the room.\n      *\n      * We can not ask the user who is leaving the room to update the participant count\n      * because user might close their browser tab without hanging up (and so without\n      * triggering the event \"videoConferenceLeft\"). So, we wait for a moment (because the\n      * event \"participantLeft\" is called many time for the participant who is leaving)\n      * and the first participant send the new participant count (so we avoid spamming the\n      * server with HTTP requests).\n      *\n      * We use \"setTimout\" to send maximum one HTTP request per interval, even if multiple\n      * participants join/leave at the same time in the defined interval.\n      *\n      * Update on the 29 June 2020\n      *\n      * @private\n      * @param {jQuery} $jitsiModal, jQuery modal element in which we add the Jitsi room\n      * @returns {JitsiRoom} the newly created Jitsi room\n      */\n    _joinJitsiRoom: async function ($parentNode) {\n        let jitsiRoom = await this._createJitsiRoom(this.roomName, $parentNode);\n\n        if (this.defaultUsername) {\n            jitsiRoom.executeCommand(\"displayName\", this.defaultUsername);\n        }\n\n        let timeoutCall = null;\n        const updateParticipantCount = (joined) => {\n            this.allParticipantIds = Object.keys(jitsiRoom._participants).sort();\n            // if we reached the maximum capacity, update immediately the participant count\n            const timeoutTime = this.allParticipantIds.length >= this.maxCapacity ? 0 : 2000;\n\n            // we clear the old timeout to be sure to call it only once each 2 seconds\n            // (so if 2 participants join/leave in this interval, we will perform only\n            // one HTTP request for both).\n            clearTimeout(timeoutCall);\n            timeoutCall = setTimeout(() => {\n                this.allParticipantIds = Object.keys(jitsiRoom._participants).sort();\n                if (this.participantId === this.allParticipantIds[0]) {\n                    // only the first participant of the room send the new participant\n                    // count so we avoid to send to many HTTP requests\n                    this._updateParticipantCount(this.allParticipantIds.length, joined);\n                }\n            }, timeoutTime);\n        };\n\n        jitsiRoom.addEventListener('participantJoined', () => updateParticipantCount(true));\n        jitsiRoom.addEventListener('participantLeft', () => updateParticipantCount(false));\n\n        // update the participant count when joining the room\n        jitsiRoom.addEventListener('videoConferenceJoined', async (event) => {\n            this.participantId = event.id;\n            updateParticipantCount(true);\n            $('.o_wjitsi_chat_room_loading').addClass('d-none');\n\n            // recheck if the room is not full\n            if (this.checkFull && this.allParticipantIds.length > this.maxCapacity) {\n                clearTimeout(timeoutCall);\n                jitsiRoom.executeCommand('hangup');\n                window.location.reload();\n            }\n        });\n\n        // update the participant count when using the \"Leave\" button\n        jitsiRoom.addEventListener('videoConferenceLeft', async (event) => {\n            this.allParticipantIds = Object.keys(jitsiRoom._participants)\n            if (!this.allParticipantIds.length) {\n                // bypass the checks and timer of updateParticipantCount\n                this._updateParticipantCount(this.allParticipantIds.length, false);\n            }\n        });\n\n        return jitsiRoom;\n    },\n\n    /**\n      * Perform an HTTP request to update the participant count on the server side.\n      *\n      * @private\n      * @param {integer} count, current number of participant in the room\n      * @param {boolean} joined, true if someone joined the room\n      */\n    _updateParticipantCount: async function (count, joined) {\n        await rpc('/jitsi/update_status', {\n            room_name: this.roomName,\n            participant_count: count,\n            joined: joined,\n        });\n    },\n\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n      * Redirect on the Jitsi mobile application if we are on mobile.\n      *\n      * @private\n      * @param {string} roomName\n      * @returns {boolean} true is we were redirected to the mobile application\n      */\n    _openMobileApplication: async function (roomName) {\n        if (uiUtils.isSmall()) {\n            // we are on mobile, open the room in the application\n            window.location = `intent://${this.jitsiServer}/${encodeURIComponent(roomName)}#Intent;scheme=org.jitsi.meet;package=org.jitsi.meet;end`;\n            return true;\n        }\n        return false;\n    },\n\n    /**\n      * Create a Jitsi room on the given DOM element.\n      *\n      * @private\n      * @param {string} roomName\n      * @param {jQuery} $parentNode\n      * @returns {JitsiRoom} the newly created Jitsi room\n      */\n    _createJitsiRoom: async function (roomName, $parentNode) {\n      await this._loadJisti();\n        const options = {\n            roomName: roomName,\n            width: \"100%\",\n            height: \"100%\",\n            parentNode: $parentNode[0],\n            configOverwrite: {disableDeepLinking: true},\n        };\n        return new window.JitsiMeetExternalAPI(this.jitsiServer, options);\n    },\n\n    /**\n      * Load the Jitsi external library if necessary.\n      *\n      * @private\n      */\n    _loadJisti: async function () {\n      if (!window.JitsiMeetExternalAPI) {\n          await $.ajax({\n              url: `https://${this.jitsiServer}/external_api.js`,\n              dataType: \"script\",\n          });\n      }\n    },\n});\n\nexport default publicWidget.registry.ChatRoom;\n", "/** @odoo-module **/\n\nimport { debounce } from \"@web/core/utils/timing\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { redirect } from \"@web/core/utils/urls\";\nimport { ExhibitorConnectClosedDialog } from \"../components/exhibitor_connect_closed_dialog/exhibitor_connect_closed_dialog\";\n\npublicWidget.registry.eventExhibitorConnect = publicWidget.Widget.extend({\n    selector: '.o_wesponsor_connect_button',\n    /**\n     * @override\n     * @public\n     */\n    init: function () {\n        this._super(...arguments);\n        this._onConnectClick = debounce(this._onConnectClick, 500, true).bind(this);\n    },\n\n    /**\n     * @override\n     * @public\n     */\n    start: function () {\n        var self = this;\n        return this._super(...arguments).then(function () {\n            self.eventIsOngoing = self.el.dataset.eventIsOngoing || false;\n            self.sponsorIsOngoing = self.el.dataset.sponsorIsOngoing || false;\n            self.isParticipating = self.el.dataset.isParticipating || false;\n            self.userEventManager = self.el.dataset.userEventManager || false;\n            self.el.addEventListener(\"click\", self._onConnectClick);\n        });\n    },\n\n    /**\n     * @override\n     * @public\n     */\n    destory () {\n        this._super(...arguments);\n        this.el.removeEventListener(\"click\", this._onConnectClick);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //-------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     * On click, if sponsor is not within opening hours, display a modal instead\n     * of redirecting on the sponsor view;\n     */\n    _onConnectClick: function (ev) {\n        ev.stopPropagation();\n        ev.preventDefault();\n\n        if (this.userEventManager) {\n            redirect(this.el.dataset.sponsorUrl);\n        } else if (!this.eventIsOngoing || ! this.sponsorIsOngoing) {\n            return this._openClosedDialog();\n        } else {\n            redirect(this.el.dataset.sponsorUrl);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _openClosedDialog: function () {\n        const sponsorId = parseInt(this.el.dataset.sponsorId);\n        this.call(\"dialog\", \"add\", ExhibitorConnectClosedDialog, { sponsorId });\n    },\n\n});\n\n\nexport default {\n    eventExhibitorConnect: publicWidget.registry.eventExhibitorConnect,\n};\n", "/** @odoo-module */\n\nimport { Component, onWillStart, markup } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { formatDuration } from \"@web/core/l10n/dates\";\n\nexport class ExhibitorConnectClosedDialog extends Component {\n    static template = \"website_event_exhibitor.ExhibitorConnectClosedDialog\";\n    static components = { Dialog };\n    static props = {\n        sponsorId: Number,\n    };\n\n    setup() {\n        onWillStart(() => this.fetchSponsor());\n    }\n\n    /**\n     * @private\n     */\n    async fetchSponsor() {\n        const sponsorData = await rpc(\n            `/event_sponsor/${encodeURIComponent(this.props.sponsorId)}/read`\n        );\n        // empty string on falsy so markup doesn't create a \"false\" string\n        sponsorData.website_description = sponsorData.website_description || \"\";\n        sponsorData.website_description = markup(sponsorData.website_description);\n        this.formatEventStartRemaining = formatDuration(sponsorData.event_start_remaining, true);\n        this.sponsorData = sponsorData;\n    }\n}\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\npublicWidget.registry.boothSponsorDetails = publicWidget.Widget.extend({\n    selector: '#o_wbooth_contact_details_form',\n    events: {\n        'click input[id=\"contact_details\"]': '_onClickContactDetails',\n    },\n\n    //--------------------------------------------------------------------------\n    // Handler\n    //--------------------------------------------------------------------------\n\n    _onClickContactDetails(ev) {\n        this.useContactDetails = ev.currentTarget.checked;\n        this.el\n            .querySelector(\"#o_wbooth_contact_details\")\n            .classList.toggle(\"d-none\", !this.useContactDetails);\n        this.el\n            .querySelectorAll(\n                \"label[for='sponsor_name'] > .mandatory_mark, label[for='sponsor_email'] > .mandatory_mark\"\n            )\n            .forEach((el) => {\n                el.classList.toggle(\"d-none\", this.useContactDetails);\n            });\n        this.el\n            .querySelectorAll(\"input[name='contact_name'], input[name='contact_email']\")\n            .forEach((inputEl) => (inputEl.required = this.useContactDetails));\n    },\n\n});\n\nexport default {\n    boothSponsorDetails: publicWidget.registry.boothSponsorDetails,\n};\n", "/** @odoo-module **/\n\nimport BoothRegistration from \"@website_event_booth/js/booth_register\";\n\n/**\n * This class changes the displayed price after selecting the requested booths.\n */\nBoothRegistration.include({\n\n    //--------------------------------------------------------------------------\n    // Overrides\n    //--------------------------------------------------------------------------\n\n    start() {\n        return this._super.apply(this, arguments).then(() => {\n            this.categoryPrice = this.selectedBoothCategory ? this.selectedBoothCategory.dataset.price : undefined;\n        });\n    },\n\n    _onChangeBoothType(ev) {\n        this.categoryPrice = parseFloat(ev.currentTarget.dataset.price);\n        return this._super.apply(this, arguments);\n    },\n\n    /**\n     * Updates the displayed total price after selecting the requested booths\n     * @param boothCount\n     * @private\n     */\n    _updateUiAfterBoothChange(boothCount) {\n        this._super.apply(this, arguments);\n        const boothTotalPriceEl = this.el.querySelector(\".o_wbooth_booth_total_price\");\n        boothTotalPriceEl?.classList.toggle(\"d-none\", !boothCount || !this.categoryPrice);\n        this._updatePrice(boothCount);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _updatePrice(boothsCount) {\n        const boothCurrencyEl = this.el.querySelector(\".o_wbooth_booth_total_price .oe_currency_value\");\n        if (boothCurrencyEl) {\n            boothCurrencyEl.textContent = `${boothsCount * this.categoryPrice}`;\n        }\n    },\n\n});\n", "/** @odoo-module **/\n\nimport { PublicWidget } from \"@web/legacy/js/public/public_widget\";\n\n/**\n * The widget will have the responsibility to manage the interactions between the\n * Youtube player and the cover containing a replay button. This widget will\n * be used when no suggestion can be found in order to hide the Youtube suggestions.\n */\nvar WebsiteEventReplaySuggestion = PublicWidget.extend({\n    template: 'website_event_track_live.website_event_track_replay_suggestion',\n    events: {\n        'click .owevent_track_suggestion_replay': '_onReplayClick'\n    },\n\n    init: function (parent, options) {\n        this._super(...arguments);\n        this.currentTrack = {\n            'name': options.current_track.name,\n            'imageSrc': options.current_track.website_image_url,\n        };\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * If the user clicks on the replay button, the function will remove the\n     * cover and send a new event to the parent to replay the video from the\n     * beginning.\n     */\n    _onReplayClick: function () {\n        this.trigger_up('replay');\n        this.destroy();\n    }\n});\n\nexport default WebsiteEventReplaySuggestion;\n", "/** @odoo-module **/\n\nimport { PublicWidget } from \"@web/legacy/js/public/public_widget\";\n\nvar WebsiteEventTrackSuggestion = PublicWidget.extend({\n    template: 'website_event_track_live.website_event_track_suggestion',\n    events: {\n        'click .owevent_track_suggestion_next': '_onNextTrackClick',\n        'click .owevent_track_suggestion_close': '_onCloseClick',\n        'click .owevent_track_suggestion_replay': '_onReplayClick'\n    },\n\n    init: function (parent, options) {\n        this._super(...arguments);\n\n        this.currentTrack = {\n            'name': options.current_track.name,\n            'imageSrc': options.current_track.website_image_url,\n        };\n        this.suggestion = {\n            'name': options.suggestion.name,\n            'speakerName': options.suggestion.speaker_name,\n            'trackUrl': options.suggestion.website_url,\n        };\n    },\n\n    start: function () {\n        var self = this;\n        this._super(...arguments).then(function () {\n            self.timerInterval = setInterval(self._updateTimer.bind(self), 1000);\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * If the user clicks on replay, remove this suggestion window and send an\n     * event to the parent so that it can rewind the video to the beginning.\n     */\n    _onReplayClick: function () {\n        this.trigger_up('replay');\n        clearInterval(this.timerInterval);\n        this.destroy();\n    },\n\n    _onCloseClick: function () {\n        clearInterval(this.timerInterval);\n        this.$('.owevent_track_suggestion_next').addClass('invisible');\n    },\n\n    _onNextTrackClick: function (ev) {\n        if ($(ev.target).hasClass('owevent_track_suggestion_close')) {\n            return;\n        }\n\n        window.location = this.suggestion.trackUrl;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _updateTimer: function () {\n        var secondsLeft = parseInt(this.$('.owevent_track_suggestion_timer_text').text());\n\n        if (secondsLeft > 1) {\n            secondsLeft -= 1;\n            this.$('.owevent_track_suggestion_timer_text').text(secondsLeft);\n        } else {\n            window.location = this.suggestion.trackUrl;\n        }\n    }\n});\n\nexport default WebsiteEventTrackSuggestion;\n", "/** @odoo-module **/\n/* global YT */\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport TrackSuggestionWidget from \"@website_event_track_live/js/website_event_track_suggestion\";\nimport ReplaySuggestionWidget from \"@website_event_track_live/js/website_event_track_replay_suggestion\";\nimport { rpc } from \"@web/core/network/rpc\";\n\npublicWidget.registry.websiteEventTrackLive = publicWidget.Widget.extend({\n    selector: '.o_wevent_event_track_live',\n    custom_events: Object.assign({}, publicWidget.Widget.prototype.custom_events, {\n        'video-ended': '_onVideoEnded'\n    }),\n\n    start: function () {\n        var self = this;\n        return this._super(...arguments).then(function () {\n            self._setupYoutubePlayer();\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    _onPlayerReady: function () {\n        this.$('.o_wevent_event_track_live_loading').remove();\n    },\n\n    _onPlayerStateChange: function (event) {\n        switch (event.data) {\n            case YT.PlayerState.ENDED:\n                this.trigger('video-ended');\n                return;\n            case YT.PlayerState.PLAYING:\n                this.trigger('video-playing');\n                return;\n            case YT.PlayerState.PAUSED:\n                this.trigger('video-paused');\n                return;\n        };\n    },\n\n    _onVideoEnded: function () {\n        this.$el.append($('<div/>', {\n            class: 'owevent_track_suggestion_loading position-absolute w-100'\n        }));\n        var self = this;\n        rpc('/event_track/get_track_suggestion', {\n            track_id: this.$el.data('trackId'),\n        }).then(function (suggestion) {\n            self.nextSuggestion = suggestion;\n            self._showSuggestion();\n        });\n    },\n\n    _onReplay: function () {\n        this.youtubePlayer.seekTo(0);\n        this.youtubePlayer.playVideo();\n        this.$('.owevent_track_suggestion_loading').remove();\n        if (this.outro) {\n            delete this.outro;\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _setupYoutubePlayer: function () {\n        var self = this;\n\n        var youtubeId = self.$el.data('youtubeVideoId');\n        var $youtubeElement = $('<script/>', {src: 'https://www.youtube.com/iframe_api'});\n        $(document.head).append($youtubeElement);\n\n        window.onYouTubeIframeAPIReady = function () {\n            self.youtubePlayer = new YT.Player('o_wevent_youtube_iframe_container', {\n                height: '100%',\n                width: '100%',\n                videoId: youtubeId,\n                playerVars: {\n                    autoplay: 1,\n                    enablejsapi: 1,\n                    rel: 0,\n                    origin: window.location.origin,\n                    widget_referrer: window.location.origin,\n                },\n                events: {\n                    'onReady': self._onPlayerReady.bind(self),\n                    'onStateChange': self._onPlayerStateChange.bind(self)\n                }\n            });\n        };\n    },\n\n    /**\n     * If a new suggestion has been found, a cover containing a replay button\n     * as well as a suggestion will automatically be placed over the Youtube\n     * player when the video ends (in non-full screen mode). If no suggestion\n     * has been found, the cover will only contain a replay button.\n     */\n    _showSuggestion: function () {\n        if (!this.outro) {\n            if (this.nextSuggestion) {\n                this.outro = new TrackSuggestionWidget(this, this.nextSuggestion);\n            } else {\n                var data = this.$el.data();\n                this.outro = new ReplaySuggestionWidget(this, {\n                    current_track: {\n                        name: data.trackName,\n                        website_image_url: data.trackWebsiteImageUrl\n                    }\n                });\n            }\n            this.outro.appendTo(this.$el);\n            this.outro.on('replay', null, this._onReplay.bind(this));\n        }\n    }\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { user } from \"@web/core/user\";\n\n/**\n * This widget is responsible of displaying quiz questions and propositions. Submitting the quiz will fetch the\n * correction and decorate the answers according to the result. Error message can be displayed.\n *\n * This widget can be attached to DOM rendered server-side by `gamification_quiz.`\n *\n */\nvar Quiz = publicWidget.Widget.extend({\n    template: 'quiz.main',\n    events: {\n        \"click .o_quiz_quiz_answer\": '_onAnswerClick',\n        \"click .o_quiz_js_quiz_submit\": '_submitQuiz',\n        \"click .o_quiz_js_quiz_reset\": '_onClickReset',\n    },\n\n    /**\n    * @override\n    * @param {Object} parent\n    * @param {Object} data holding all the container information\n    * @param {Object} quizData : quiz data to display\n    */\n    init: function (parent, data, quizData) {\n        this._super.apply(this, arguments);\n        this.track = Object.assign({\n            id: 0,\n            name: '',\n            eventId: '',\n            completed: false,\n            isMember: false,\n            progressBar: false,\n            isEventUser: false,\n            repeatable: false\n        }, data);\n        this.quiz = quizData || false;\n        if (this.quiz) {\n            this.quiz.questionsCount = quizData.questions.length;\n        }\n        this.isMember = data.isMember || false;\n        this.userId = user.userId;\n        this.redirectURL = encodeURIComponent(document.URL);\n\n        this.notification = this.bindService(\"notification\");\n    },\n\n    /**\n     * @override\n     */\n    willStart: function () {\n        var defs = [this._super.apply(this, arguments)];\n        if (!this.quiz) {\n            defs.push(this._fetchQuiz());\n        }\n        return Promise.all(defs);\n    },\n\n    /**\n     * Overridden to add custom rendering behavior upon start of the widget.\n     *\n     * If the user has answered the quiz before having joined the course, we check\n     * their answers (saved into their session) here as well.\n     *\n     * @override\n     */\n    start: function () {\n        var self = this;\n        return this._super.apply(this, arguments).then(function ()  {\n            self._renderValidationInfo();\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _alertShow: function (alertCode) {\n        var message = _t('There was an error validating this quiz.');\n        if (alertCode === 'quiz_incomplete') {\n            message = _t('All questions must be answered!');\n        } else if (alertCode === 'quiz_done') {\n            message = _t('This quiz is already done. Retaking it is not possible.');\n        }\n\n        this.notification.add(message, {\n            type: 'warning',\n            title: _t('Quiz validation error'),\n            sticky: true\n        });\n    },\n\n    /**\n     * @private\n     * Decorate the answers according to state\n     */\n    _disableAnswers: function () {\n        this.$('.o_quiz_js_quiz_question').addClass('completed-disabled');\n        this.$('input[type=radio]').prop('disabled', true);\n    },\n\n    /**\n     * @private\n     * Decorate the answers according to state\n     */\n    _enableAnswers: function() {\n        this.$('.o_quiz_js_quiz_question').removeClass('completed-disabled');\n        this.$('input[type=radio]').prop('disabled', false);\n    },\n\n    /**\n     * Get all the questions ID from the displayed Quiz\n     * @returns {Array}\n     * @private\n     */\n    _getQuestionsIds: function () {\n        return this.$('.o_quiz_js_quiz_question').map(function () {\n            return $(this).data('question-id');\n        }).get();\n    },\n\n    /**\n     * Get the quiz answers filled in by the User\n     *\n     * @private\n     */\n    _getQuizAnswers: function () {\n        return this.$('input[type=radio]:checked').map(function (index, element) {\n            return parseInt($(element).val());\n        }).get();\n    },\n\n    /**\n     * Decorate the answer inputs according to the correction and adds the answer comment if\n     * any.\n     *\n     * @private\n     */\n    _renderAnswersHighlightingAndComments: function () {\n        var self = this;\n        this.$('.o_quiz_js_quiz_question').each(function () {\n            var $question = $(this);\n            var questionId = $question.data('questionId');\n            var answer = self.quiz.answers[questionId];\n            $question.find('a.o_quiz_quiz_answer').each(function () {\n                var $answer = $(this);\n                $answer.find('i.fa').addClass('d-none');\n                if ($answer.find('input[type=radio]').is(':checked')) {\n                    if (answer.is_correct) {\n                        $answer.find('i.fa-check-circle').removeClass('d-none');\n                    } else {\n                        $answer.find('label input').prop('checked', false);\n                        $answer.find('i.fa-times-circle').removeClass('d-none');\n                    }\n                    if (answer.awarded_points > 0) {\n                        $answer.append(renderToElement('quiz.badge', {'answer': answer}));\n                    }\n                } else {\n                    $answer.find('i.fa-circle').removeClass('d-none');\n                }\n            });\n            var $list = $question.find('.list-group');\n            $list.append(renderToElement('quiz.comment', {'answer': answer}));\n        });\n    },\n\n    /*\n        * @private\n        * Update validation box (karma, buttons) according to widget state\n        */\n    _renderValidationInfo: function () {\n        var $validationElem = this.$('.o_quiz_js_quiz_validation');\n        $validationElem.empty().append(\n            renderToElement('quiz.validation', {'widget': this})\n        );\n    },\n\n    /**\n     * Remove the answer decorators\n     */\n     _resetQuiz: function () {\n        this.$('.o_quiz_js_quiz_question').each(function () {\n            var $question = $(this);\n            $question.find('a.o_quiz_quiz_answer').each(function () {\n                var $answer = $(this);\n                $answer.find('i.fa').addClass('d-none');\n                $answer.find('i.fa-circle').removeClass('d-none');\n                $answer.find('span.badge').remove();\n                $answer.find('input[type=radio]').prop('checked', false);\n            });\n            var $info = $question.find('.o_quiz_quiz_answer_info');\n            $info.remove();\n        });\n        this.track.completed = false;\n        this._enableAnswers();\n        this._renderValidationInfo();\n    },\n\n    /**\n     * Submit a quiz and get the correction. It will display messages\n     * according to quiz result.\n     *\n     * @private\n     */\n    _submitQuiz: function () {\n        var self = this;\n\n        return rpc('/event_track/quiz/submit', {\n            event_id: self.track.eventId,\n            track_id: self.track.id,\n            answer_ids: this._getQuizAnswers(),\n        }).then(function (data) {\n            if (data.error) {\n                self._alertShow(data.error);\n            } else {\n                self.quiz = Object.assign(self.quiz, data);\n                self.quiz.quizPointsGained = data.quiz_points;\n                if (data.quiz_completed) {\n                    self._disableAnswers();\n                    self.track.completed = data.quiz_completed;\n                }\n                self._renderAnswersHighlightingAndComments();\n                self._renderValidationInfo();\n            }\n\n            return Promise.resolve(data);\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * When clicking on an answer, this one should be marked as \"checked\".\n     *\n     * @private\n     * @param OdooEvent ev\n     */\n    _onAnswerClick: function (ev) {\n        ev.preventDefault();\n        if (!this.track.completed) {\n            $(ev.currentTarget).find('input[type=radio]').prop('checked', true);\n        }\n    },\n\n    /**\n     * Resets the completion of the track so the user can take\n     * the quiz again\n     *\n     * @private\n     */\n    _onClickReset: function () {\n        rpc('/event_track/quiz/reset', {\n            event_id: this.track.eventId,\n            track_id: this.track.id\n        }).then(this._resetQuiz.bind(this));\n    },\n\n});\n\npublicWidget.registry.Quiz = publicWidget.Widget.extend({\n    selector: '.o_quiz_main',\n\n    //----------------------------------------------------------------------\n    // Public\n    //----------------------------------------------------------------------\n\n    /**\n     * @override\n     * @param {Object} parent\n     */\n    start: function () {\n        var self = this;\n        this.quizWidgets = [];\n        var defs = [this._super.apply(this, arguments)];\n        this.$('.o_quiz_js_quiz').each(function () {\n            var data = $(this).data();\n            data.quizData = {\n                questions: self._extractQuestionsAndAnswers(),\n                sessionAnswers: data.sessionAnswers || [],\n                quizKarmaMax: data.quizKarmaMax,\n                quizKarmaWon: data.quizKarmaWon,\n                quizKarmaGain: data.quizKarmaGain,\n                quizPointsGained: data.quizPointsGained,\n                quizAttemptsCount: data.quizAttemptsCount,\n            };\n            defs.push(new Quiz(self, data, data.quizData).attachTo($(this)));\n        });\n        return Promise.all(defs);\n    },\n\n    //----------------------------------------------------------------------\n    // Private\n    //---------------------------------------------------------------------\n\n    /**\n     * Extract data from exiting DOM rendered server-side, to have the list of questions with their\n     * relative answers.\n     * This method should return the same format as /gamification_quiz/quiz/get controller.\n     *\n     * @return {Array<Object>} list of questions with answers\n     */\n    _extractQuestionsAndAnswers: function () {\n        var questions = [];\n        this.$('.o_quiz_js_quiz_question').each(function () {\n            var $question = $(this);\n            var answers = [];\n            $question.find('.o_quiz_quiz_answer').each(function () {\n                var $answer = $(this);\n                answers.push({\n                    id: $answer.data('answerId'),\n                    text: $answer.data('text'),\n                });\n            });\n            questions.push({\n                id: $question.data('questionId'),\n                title: $question.data('title'),\n                answer_ids: answers,\n            });\n        });\n        return questions;\n    },\n});\n\nexport default Quiz;\n", "/** @odoo-module **/\n\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\npublicWidget.registry.EventLeaderboard = publicWidget.Widget.extend({\n    selector: '.o_wevent_quiz_leaderboard',\n\n    /**\n     * Basic override to scroll to current visitor's position.\n     */\n    start: function () {\n        var self = this;\n        return this._super(...arguments).then(function () {\n            var $scrollTo = self.$('.o_wevent_quiz_scroll_to');\n            if ($scrollTo.length !== 0) {\n                var offset = $('.o_header_standard').height();\n                var $appMenu = $('.o_main_navbar');\n                if ($appMenu.length !== 0) {\n                    offset += $appMenu.height();\n                }\n                window.scrollTo({\n                    top: $scrollTo.offset().top - offset,\n                    behavior: 'smooth'\n                });\n            }\n        });\n    }\n});\n\nexport default publicWidget.registry.EventLeaderboard;\n", "/** @odoo-module **/\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport Quiz from \"@website_event_track_quiz/js/event_quiz\";\n\nvar WebsiteEventTrackSuggestionQuiz = Quiz.include({\n    /**\n     * @override\n     */\n    willStart: function () {\n        return Promise.all([\n            this._super(...arguments),\n            this._getTrackSuggestion()\n        ]);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _submitQuiz: function () {\n        var self = this;\n        return this._super(...arguments).then(function (data) {\n            if (data.quiz_completed) {\n                self.$('.o_quiz_js_quiz_next_track')\n                    .removeClass('btn-light')\n                    .addClass('btn-secondary');\n            }\n\n            return Promise.resolve(data);\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    _getTrackSuggestion: function () {\n        var self = this;\n        return rpc('/event_track/get_track_suggestion', {\n            track_id: this.track.id,\n        }).then(function (suggestion) {\n            self.nextSuggestion = suggestion;\n            return Promise.resolve();\n        });\n    }\n});\n\nexport default WebsiteEventTrackSuggestionQuiz;\n", "/** @odoo-module **/\n\nimport WebsiteEventTrackSuggestion from \"@website_event_track_live/js/website_event_track_suggestion\";\n\nvar WebsiteEventTrackSuggestionLiveQuiz = WebsiteEventTrackSuggestion.include({\n    events: Object.assign({}, WebsiteEventTrackSuggestion.prototype.events, {\n        'click .owevent_track_suggestion_quiz': '_onQuizClick'\n    }),\n\n    init: function (parent, options) {\n        this._super(...arguments);\n        this.currentTrack.showQuiz = options.current_track.show_quiz;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * If the user takes the quiz, stop the next suggestion timer\n     */\n    _onQuizClick: function () {\n        clearInterval(this.timerInterval);\n        this.$('.owevent_track_suggestion_timer_text_wrapper').remove();\n    }\n});\n\nexport default WebsiteEventTrackSuggestionLiveQuiz;\n", "import {\n    App,\n    Component,\n    onMounted,\n    onWillStart,\n    onWillUnmount,\n    onWillUpdateProps,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\nimport { getBundle } from \"@web/core/assets\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { TableOfContentManager } from \"@html_editor/others/embedded_components/core/table_of_content/table_of_content_manager\";\n\nexport class HtmlViewer extends Component {\n    static template = \"html_editor.HtmlViewer\";\n    static props = {\n        config: { type: Object },\n    };\n    static defaultProps = {\n        hasFullHtml: false,\n    };\n\n    setup() {\n        this.iframeRef = useRef(\"iframe\");\n\n        this.state = useState({\n            iframeVisible: false,\n            value: this.formatValue(this.props.config.value),\n        });\n        this.components = new Set();\n\n        onWillUpdateProps((newProps) => {\n            const newValue = this.formatValue(newProps.config.value);\n            if (newValue.toString() !== this.state.value.toString()) {\n                this.state.value = this.formatValue(newProps.config.value);\n                if (this.props.config.embeddedComponents) {\n                    this.destroyComponents();\n                }\n                if (this.showIframe) {\n                    this.updateIframeContent(this.state.value);\n                }\n            }\n        });\n\n        onWillUnmount(() => {\n            this.destroyComponents();\n        });\n\n        if (this.showIframe) {\n            onMounted(() => {\n                const onLoadIframe = () => this.onLoadIframe(this.state.value);\n                this.iframeRef.el.addEventListener(\"load\", onLoadIframe, { once: true });\n                // Force the iframe to call the `load` event. Without this line, the\n                // event 'load' might never trigger.\n                this.iframeRef.el.after(this.iframeRef.el);\n            });\n        } else {\n            this.readonlyElementRef = useRef(\"readonlyContent\");\n            useEffect(() => {\n                this.retargetLinks(this.readonlyElementRef.el);\n            });\n        }\n\n        if (this.props.config.cssAssetId) {\n            onWillStart(async () => {\n                this.cssAsset = await getBundle(this.props.config.cssAssetId);\n            });\n        }\n\n        if (this.props.config.embeddedComponents) {\n            // TODO @phoenix: should readonly iframe with embedded components be supported?\n            this.embeddedComponents = memoize((embeddedComponents = []) => {\n                const result = {};\n                for (const embedding of embeddedComponents) {\n                    result[embedding.name] = embedding;\n                }\n                return result;\n            });\n            useEffect(\n                () => {\n                    if (this.readonlyElementRef?.el) {\n                        this.mountComponents();\n                    }\n                },\n                () => [this.props.config.value.toString(), this.readonlyElementRef?.el]\n            );\n            this.tocManager = new TableOfContentManager(this.readonlyElementRef);\n        }\n    }\n\n    get showIframe() {\n        return this.props.config.hasFullHtml || this.props.config.cssAssetId;\n    }\n\n    /**\n     * Allows overrides to process the value used in the Html Viewer.\n     *\n     * @param { Markup } value\n     * @returns { Markup }\n     */\n    formatValue(value) {\n        return value;\n    }\n\n    /**\n     * Ensure all links are opened in a new tab.\n     */\n    retargetLinks(container) {\n        for (const link of container.querySelectorAll(\"a\")) {\n            this.retargetLink(link);\n        }\n    }\n\n    retargetLink(link) {\n        link.setAttribute(\"target\", \"_blank\");\n        link.setAttribute(\"rel\", \"noreferrer\");\n    }\n\n    updateIframeContent(content) {\n        const contentWindow = this.iframeRef.el.contentWindow;\n        const iframeTarget = this.props.config.hasFullHtml\n            ? contentWindow.document.documentElement\n            : contentWindow.document.querySelector(\"#iframe_target\");\n        iframeTarget.innerHTML = content;\n        this.retargetLinks(iframeTarget);\n    }\n\n    onLoadIframe(value) {\n        const contentWindow = this.iframeRef.el.contentWindow;\n        if (!this.props.config.hasFullHtml) {\n            contentWindow.document.open(\"text/html\", \"replace\").write(\n                `<!DOCTYPE html><html>\n                        <head>\n                            <meta charset=\"utf-8\"/>\n                            <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n                            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\"/>\n                        </head>\n                        <body class=\"o_in_iframe o_readonly\" style=\"overflow: hidden;\">\n                            <div id=\"iframe_target\"></div>\n                        </body>\n                    </html>`\n            );\n        }\n\n        if (this.cssAsset) {\n            for (const cssLib of this.cssAsset.cssLibs) {\n                const link = contentWindow.document.createElement(\"link\");\n                link.setAttribute(\"type\", \"text/css\");\n                link.setAttribute(\"rel\", \"stylesheet\");\n                link.setAttribute(\"href\", cssLib);\n                contentWindow.document.head.append(link);\n            }\n        }\n\n        this.updateIframeContent(this.state.value);\n        this.state.iframeVisible = true;\n    }\n\n    //--------------------------------------------------------------------------\n    // Embedded Components\n    //--------------------------------------------------------------------------\n\n    destroyComponent({ app, host }) {\n        const { getEditableDescendants } = this.getEmbedding(host);\n        const editableDescendants = getEditableDescendants?.(host) || {};\n        app.destroy();\n        this.components.delete(arguments[0]);\n        host.append(...Object.values(editableDescendants));\n    }\n\n    destroyComponents() {\n        for (const info of [...this.components]) {\n            this.destroyComponent(info);\n        }\n    }\n\n    forEachEmbeddedComponentHost(elem, callback) {\n        const selector = `[data-embedded]`;\n        const targets = [...elem.querySelectorAll(selector)];\n        if (elem.matches(selector)) {\n            targets.unshift(elem);\n        }\n        for (const host of targets) {\n            const embedding = this.getEmbedding(host);\n            if (!embedding) {\n                continue;\n            }\n            callback(host, embedding);\n        }\n    }\n\n    getEmbedding(host) {\n        return this.embeddedComponents(this.props.config.embeddedComponents)[host.dataset.embedded];\n    }\n\n    setupNewComponent({ name, env, props }) {\n        if (name === \"tableOfContent\") {\n            Object.assign(props, {\n                manager: this.tocManager,\n            });\n        }\n    }\n\n    mountComponent(host, { Component, getEditableDescendants, getProps, name }) {\n        const props = getProps?.(host) || {};\n        const mainApp = this.__owl__.app;\n        const { dev, translateFn, getRawTemplate } = mainApp;\n        // TODO ABD TODO @phoenix: check if there is too much info in the htmlViewer env.\n        // i.e.: env has X because of parent component,\n        // embedded component descendant sometimes uses X from env which is set conditionally:\n        // -> it will override the one one from the parent => OK.\n        // -> it will not => the embedded component still has X in env because of its ancestors => Issue.\n        const env = Object.create(this.env);\n        if (getEditableDescendants) {\n            env.getEditableDescendants = getEditableDescendants;\n        }\n        this.setupNewComponent({\n            name,\n            env,\n            props,\n        });\n        const app = new App(Component, {\n            test: dev,\n            env,\n            translateFn,\n            getTemplate: getRawTemplate,\n            props,\n        });\n        app.rawTemplates = mainApp.rawTemplates;\n        // Can't copy compiled templates because they have a reference to the main app\n        // app.templates = mainApp.templates;\n        const promise = app.mount(host);\n        // Don't show mounting errors as they will happen often when the host\n        // is disconnected from the DOM because of a patch\n        promise.catch();\n        // Patch mount fiber to hook into the exact call stack where app is\n        // mounted (but before). This will remove host children synchronously\n        // just before adding the app rendered html.\n        const fiber = Array.from(app.scheduler.tasks)[0];\n        const fiberComplete = fiber.complete;\n        fiber.complete = function () {\n            host.replaceChildren();\n            fiberComplete.call(this);\n        };\n        const info = {\n            app,\n            host,\n        };\n        this.components.add(info);\n    }\n\n    mountComponents() {\n        this.forEachEmbeddedComponentHost(this.readonlyElementRef.el, (host, embedding) => {\n            this.mountComponent(host, embedding);\n        });\n    }\n}\n", "import {\n    onMounted,\n    onRendered,\n    onPatched,\n    onWillDestroy,\n    reactive,\n    toRaw,\n    useComponent,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\n/**\n * @typedef {HTMLElement} HostElement host element for an embedded component\n * @typedef {Object} State state obtained from `useState` usage\n * @typedef {Record<string, HTMLElement>} EditableDescendants\n * @typedef {(state, previous, next) => void} PropertyUpdate function applying\n *          a state change which can be computed from `previous` and `next`\n *          to `state`.\n * @typedef {Record<string, PropertyUpdate>} PropertyUpdater\n *\n * @typedef {Object} StateChangeManagerConfig\n * @property {PropertyUpdater} [propertyUpdater] object mapping a key of the\n *        state to a function which will compute how values from a stateChange\n *        are applied to the current state. Defined in the embedding definition\n *        of a component.\n * @property {function(HostElement):State} [getEmbeddedState]\n *        custom function to get the first embedded state (the one used during\n *        setup), in case not all embedded props should be part of the state, or\n *        if more properties should be added to it.\n * @property {function(HostElement, State):Object} [stateToEmbeddedProps]\n *        custom function to compute the props, i.e. in case the entire state\n *        should not be converted to props.\n *\n * @typedef {Object} Embedding object provided to the instance which mounts\n *          Embedded components (EmbeddedComponentPlugin, HtmlViewer, ...)\n * @property {String} name\n * @property {Component} Component\n * @property {function(HostElement):Object} getProps props for the given\n *           Component class instance.\n * @property {function(HostElement):EditableDescendants} [getEditableDescendants]\n *           @see useEditableDescendants\n * @property {function(StateChangeManagerConfig):StateChangeManager} [getStateChangeManager]\n *           @see useEmbeddedState\n */\n\n/**\n * Get all element children with `data-embedded-editable` attribute which are\n * descendants of the host's own embedded component and not part of another\n * embedded component descendant (an embedded component can contain others).\n * If multiple elements have the same `data-embedded-editable`, only the last\n * one is considered.\n * @param {HostElement} host\n * @returns {EditableDescendants} editableDescendants\n */\nexport function getEditableDescendants(host) {\n    const editableDescendants = {};\n    for (const candidate of host.querySelectorAll(\"[data-embedded-editable]\")) {\n        if (candidate.closest(\"[data-embedded]\") === host) {\n            editableDescendants[candidate.dataset.embeddedEditable] = candidate;\n        }\n    }\n    return editableDescendants;\n}\n\n/**\n * Handle the rendering of editableDescendants:\n * It is a node owned by the editor, which will be inserted under a ref of\n * the same name as the attribute `data-embedded-editable` of that node, in the\n * component's template. This allows to use editor features inside an embedded\n * component. EditableDescendants are shared in collaboration and are saved\n * between edition sessions.\n *\n * Warning: there must be a ref in the template for every editableDescendants,\n * available at all times no matter the component state to guarantee that the\n * editor can save their values at any given time, synchronously.\n *\n * @param {HostElement} host\n * @returns {EditableDescendants} (HTMLElement) by the value of their\n *          `data-embedded-editable` attribute.\n */\nexport function useEditableDescendants(host) {\n    const component = useComponent();\n    if (!component.env.getEditableDescendants) {\n        throw new Error(\n            \"Missing `getEditableDescendants` function in the `embedding` provided to the `EmbeddedComponentPlugin`.\"\n        );\n    }\n    const editableDescendants = Object.freeze(component.env.getEditableDescendants(host));\n    const refs = {};\n    const renders = {};\n    for (const name of Object.keys(editableDescendants)) {\n        refs[name] = useRef(name);\n        renders[name] = () => refs[name].el.replaceChildren(editableDescendants[name]);\n    }\n    let _restoreSelection;\n    const restoreSelection = () => {\n        if (_restoreSelection) {\n            _restoreSelection();\n            _restoreSelection = undefined;\n        }\n    };\n    if (component.env.editorShared?.preserveSelection) {\n        onRendered(() => {\n            _restoreSelection = component.env.editorShared.preserveSelection().restore;\n        });\n    }\n    onMounted(() => {\n        for (const render of Object.values(renders)) {\n            render();\n        }\n        restoreSelection();\n    });\n    onPatched(() => {\n        for (const [name, render] of Object.entries(renders)) {\n            // Handle partial patch\n            if (!host.contains(editableDescendants[name])) {\n                render();\n            }\n        }\n        restoreSelection();\n    });\n    return editableDescendants;\n}\n\n/**\n * Create a ProxyHandler to manage a serializable \"buffer\" (Proxy target) for\n * changes. The buffer must be a @see reactive which should update state\n * with its callback (commit).\n * @see useEmbeddedState\n * The Proxy target and state must be serializable through JSON.stringify.\n *\n * @param {Object} state\n * @param {Object} stateChangeManager\n * @param {Object} stateChangeManager.previousEmbeddedState null, or a deep copy\n *        of the target used as a reference point for comparison\n *        (before <-> after) so that multiple synchronous changes can be handled\n *        at once.\n * @returns {ProxyHandler}\n */\nfunction embeddedStateProxyHandler(state, stateChangeManager) {\n    return {\n        // Write operations are always done on the target (\"buffer\").\n        // During the first write operation before a commit, keep a deep copy of\n        // the target through serialization, which will be used as a reference\n        // point for a comparison (before <-> after).\n        set(target, key, value, receiver) {\n            if (\n                value !== Reflect.get(target, key, receiver) &&\n                !stateChangeManager.previousEmbeddedState\n            ) {\n                stateChangeManager.previousEmbeddedState = JSON.parse(\n                    JSON.stringify(stateChangeManager.embeddedState)\n                );\n            }\n            return Reflect.set(target, key, value, receiver);\n        },\n        deleteProperty(target, key) {\n            if (Reflect.has(target, key) && !stateChangeManager.previousEmbeddedState) {\n                stateChangeManager.previousEmbeddedState = JSON.parse(\n                    JSON.stringify(stateChangeManager.embeddedState)\n                );\n            }\n            return Reflect.deleteProperty(target, key);\n        },\n        // Read operations should also be done on state to register the\n        // rendering callback.\n        get(target, key, receiver) {\n            Reflect.get(state, key, state);\n            return Reflect.get(target, key, receiver);\n        },\n        ownKeys(target) {\n            Reflect.ownKeys(state);\n            return Reflect.ownKeys(target);\n        },\n        has(target, key) {\n            Reflect.has(state, key);\n            return Reflect.has(target, key);\n        },\n    };\n}\n\nfunction observeAllKeys(reactive) {\n    for (const key in reactive) {\n        const prop = reactive[key];\n        if (prop instanceof Object) {\n            observeAllKeys(prop);\n        }\n    }\n}\n\n/**\n * Extract props serialized in `data-embedded-props` attribute.\n *\n * @param {HostElement} host\n * @returns {Object} props\n */\nexport function getEmbeddedProps(host) {\n    return host.dataset.embeddedProps ? JSON.parse(host.dataset.embeddedProps) : {};\n}\n\nfunction sortedCopy(obj) {\n    const result = {};\n    const propNames = Object.keys(obj).sort();\n    for (const propName of propNames) {\n        result[propName] = obj[propName];\n    }\n    return result;\n}\n\n/**\n * Compute the difference between next and previous, and apply that difference\n * to container[key]. Comparison is done through JSON.stringify, so all values\n * must be serializable.\n *\n * @param {Object} container\n * @param {string} key\n * @param {Object} previous\n * @param {Object} next\n */\nexport function applyObjectPropertyDifference(container, key, previous, next) {\n    if (!container[key]) {\n        container[key] = {};\n    }\n    const obj1 = { ...(previous || {}) };\n    const obj2 = { ...(next || {}) };\n    const dest = container[key];\n    for (const key in obj2) {\n        if (JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {\n            dest[key] = obj2[key];\n        }\n        delete obj1[key];\n    }\n    for (const key in obj1) {\n        delete dest[key];\n    }\n    if (!Object.keys(dest).length && !next) {\n        delete container[key];\n    }\n}\n\n/**\n * Overwrite container[key] with value.\n *\n * @param {Object} container\n * @param {string} key\n * @param {Object} value\n */\nexport function replaceProperty(container, key, value) {\n    if (value === undefined) {\n        delete container[key];\n    } else {\n        container[key] = value;\n    }\n}\n\nexport class StateChangeManager {\n    /**\n     * @param {StateChangeManagerConfig} config\n     * @param {HostElement} config.host\n     * @param {Function} config.dispatch plugin dispatch to send editor commands\n     */\n    constructor(config) {\n        this.config = config;\n    }\n    setup() {\n        const defaultState = sortedCopy(this.getEmbeddedState());\n        const defaultStateChange = {\n            stateChangeId: null,\n            previous: defaultState,\n            next: defaultState,\n        };\n        // Used in case `data-embedded-state` is removed (i.e. when reverting\n        // the first mutation setting that attribute)\n        this.defaultStateChange = defaultStateChange;\n        // Used to keep track of the last applied stateChange, to avoid\n        // applying it multiple times (i.e. revertMutations + stageRecords\n        // during undo)\n        this.previousStateChange = defaultStateChange;\n        // Used to discard batch changes when a component is destroyed,\n        // pending state changes should not be applied\n        this.batchId = 0;\n        this.setupUnmounted();\n    }\n\n    /**\n     * Called at setup and when an embedded component is destroyed. This resets\n     * state values related to the mounted component. State changes will be\n     * handled differently when unmounted.\n     */\n    setupUnmounted() {\n        this.previousEmbeddedState = null;\n        this.state = null;\n        this.embeddedState = null;\n        this.embeddedStateProxy = null;\n        this.isLiveComponent = false;\n        this.batchId += 1;\n    }\n\n    /**\n     * Construct the proxy object to use inside an embedded component. It can\n     * be read on to register for rendering updates in the component template,\n     * and written on to trigger a re-rendering, sharing changes in\n     * collaboration and registering them for the history.\n     * @param {Object} state\n     * @returns {Proxy} embeddedStateProxy\n     */\n    constructEmbeddedState(state) {\n        this.state = state;\n        this.embeddedState = reactive(\n            this.assignDeepProxyCopy({}, state),\n            this.batchedChangeState()\n        );\n        this.embeddedStateProxy = new Proxy(\n            this.embeddedState,\n            embeddedStateProxyHandler(state, this)\n        );\n        // First subscription to changes.\n        observeAllKeys(this.embeddedStateProxy);\n        this.isLiveComponent = true;\n        return this.embeddedStateProxy;\n    }\n\n    /**\n     * Depending on whether the component is destroyed or started mounting,\n     * return its effective state.\n     * @returns {Object} state\n     */\n    getState() {\n        let state = this.state;\n        if (!this.isLiveComponent) {\n            state = this.getEmbeddedState();\n        }\n        return state;\n    }\n\n    /**\n     * Called when `data-embedded-state` attribute is being changed. This\n     * will update the state, the embedded state, the embedded props and\n     * recompute a new expression when necessary.\n     * @param {string} attrState JSON representation of a stateChange\n     * @param { Object } options\n     * @param {boolean} options.reverse whether to read the stateChange from\n     *        next to previous\n     * @param {boolean} options.forNewStep whether the attribute change is being\n     *        used to create a new step.\n     * @returns {string} new JSON representation of a stateChange, in case\n     *          it needs to be represented under another form to be shared\n     *          in collaboration (a local peer doing revertMutations implies\n     *          that collaborators will do applyMutations, so the stateChange\n     *          must be expressed with another form for them).\n     */\n    onStateChanged(attrState, { reverse = false, forNewStep = false } = {}) {\n        const stateChange = attrState ? JSON.parse(attrState) : this.defaultStateChange;\n        const state = this.getState();\n        if (reverse) {\n            this.reverseStateChange(stateChange);\n        }\n        if (!this.areStateChangesEqual(this.previousStateChange, stateChange)) {\n            const previous = JSON.stringify(sortedCopy(state));\n            this.commitStateChange(state, stateChange.previous, stateChange.next);\n            const sortedState = sortedCopy(state);\n            this.config.host.dataset.embeddedProps = JSON.stringify(\n                this.stateToEmbeddedProps(this.config.host, sortedState)\n            );\n            if (this.isLiveComponent && !this.previousEmbeddedState) {\n                // Update the embeddedState only if there is no pending change.\n                // If there is a pending change, it will be updated when the\n                // pending change is applied in `changeState`.\n                this.assignDeepProxyCopy(toRaw(this.embeddedState), sortedState);\n            }\n            if (!forNewStep) {\n                this.previousStateChange = stateChange;\n            } else {\n                // If mutations are being applied to create a new step, the\n                // state change must be expressed under another form for\n                // collaborators, since the collaborator will always\n                // \"applyMutations\" and never \"revertMutations\" when receiving\n                // external steps.\n                const next = JSON.stringify(sortedState);\n                if (previous !== next) {\n                    this.previousStateChange = {\n                        stateChangeId: this.generateId(),\n                        previous: JSON.parse(previous),\n                        next: JSON.parse(next),\n                    };\n                    return JSON.stringify(this.previousStateChange);\n                }\n            }\n        }\n    }\n\n    /**\n     * Allow to write on the embeddedState multiple times synchronously\n     * and batch all changes at once afterwards. A batch is discarded as soon\n     * as the component is destroyed.\n     * @returns {Function} batched changeState\n     */\n    batchedChangeState() {\n        let scheduled = false;\n        const batchId = this.batchId;\n        return async () => {\n            if (this.isLiveComponent && !scheduled) {\n                scheduled = true;\n                await Promise.resolve();\n                scheduled = false;\n                if (batchId === this.batchId) {\n                    this.changeState();\n                }\n            }\n        };\n    }\n\n    /**\n     * Apply a stateChange that was done on the embeddedState to the state,\n     * to trigger a re-rendering, and write the stateChange in\n     * `data-embedded-state` for the history and collaboration. Also\n     * recompute `data-embedded-props` for the next mounting operation.\n     */\n    changeState() {\n        const previousEmbeddedState = this.previousEmbeddedState;\n        this.previousEmbeddedState = null;\n        const previous = JSON.stringify(sortedCopy(this.state));\n        this.commitStateChange(\n            this.state,\n            previousEmbeddedState,\n            JSON.parse(JSON.stringify(this.embeddedState))\n        );\n        const sortedState = sortedCopy(this.state);\n        const next = JSON.stringify(sortedState);\n        this.assignDeepProxyCopy(toRaw(this.embeddedState), sortedState);\n        if (previous !== next) {\n            this.previousStateChange = {\n                stateChangeId: this.generateId(),\n                previous: JSON.parse(previous),\n                next: JSON.parse(next),\n            };\n            this.config.host.dataset.embeddedState = JSON.stringify(this.previousStateChange);\n            this.config.host.dataset.embeddedProps = JSON.stringify(\n                this.stateToEmbeddedProps(this.config.host, sortedState)\n            );\n            this.config.dispatch(\"ADD_STEP\");\n        }\n        observeAllKeys(this.embeddedStateProxy);\n    }\n\n    areStateChangesEqual(sc1, sc2) {\n        return (\n            sc1.stateChangeId === sc2.stateChangeId &&\n            JSON.stringify(sc1.previous) === JSON.stringify(sc2.previous) &&\n            JSON.stringify(sc1.next) === JSON.stringify(sc2.next)\n        );\n    }\n\n    reverseStateChange(stateChange) {\n        const previous = stateChange.previous;\n        stateChange.previous = stateChange.next;\n        stateChange.next = previous;\n    }\n\n    /**\n     * Replace every key of target with deep proxy copies of source.\n     * This will make it so that any change at any level will pass by the\n     * embeddedStateProxyHandler traps.\n     * @param {Object} target\n     * @param {Object} source\n     * @returns {Object} copy with proxies as keys\n     */\n    assignDeepProxyCopy(target, source) {\n        for (const key of Object.keys(target)) {\n            delete target[key];\n        }\n        for (const key of Object.keys(source)) {\n            target[key] = this.deepProxyCopy(source[key]);\n        }\n        return target;\n    }\n\n    /**\n     * Create a deep proxy copy of value ensuring that any change at any level\n     * will pass by the embeddedStateProxyHandler traps.\n     * @param {Object} value\n     * @returns {Proxy} deep proxy copy of value\n     */\n    deepProxyCopy(value) {\n        if (value instanceof Object) {\n            const copy = value instanceof Array ? [] : {};\n            for (const prop in value) {\n                copy[prop] = this.deepProxyCopy(value[prop]);\n            }\n            return new Proxy(copy, embeddedStateProxyHandler(value, this));\n        }\n        return value;\n    }\n\n    generateId() {\n        return Math.floor(Math.random() * Math.pow(2, 52));\n    }\n\n    /**\n     * Apply a transaction to the active state. `previous` is the state\n     * before the transaction, and `next` is the state after the\n     * transaction was done. Keep in mind that the current state may have\n     * been changed after the transaction was done, but before it was\n     * applied. By default, will always accept nextState as\n     * the final state. `propertyUpdater` should be provided in the config\n     * to handle some keys differently, i.e. object composition.\n     * @see applyObjectPropertyDifference\n     * @param {Object} state current state\n     * @param {Object} previous state before the transaction\n     * @param {Object} next state after the transaction\n     */\n    commitStateChange(state, previous, next) {\n        const currentKeys = new Set([\n            ...Object.keys(state),\n            ...Object.keys(previous),\n            ...Object.keys(next),\n        ]);\n        for (const key of currentKeys) {\n            if (key in (this.config.propertyUpdater || {})) {\n                this.config.propertyUpdater[key](state, previous, next);\n            } else if (JSON.stringify(previous[key]) !== JSON.stringify(next[key])) {\n                replaceProperty(state, key, next[key]);\n            }\n        }\n    }\n\n    /**\n     * Extract values to be used as the first embedded state (used for setup)\n     * from the host.\n     * Extract all values from `data-embedded-props` by default.\n     * @returns {Object} state\n     */\n    getEmbeddedState() {\n        const host = this.config.host;\n        return this.config.getEmbeddedState?.(host) || getEmbeddedProps(host);\n    }\n\n    /**\n     * Convert a state to an object containing the props to be\n     * saved in `data-embedded-props`, which will be used for the next mount\n     * operation, and saved in the database. The returned object should be\n     * serializable using JSON.\n     * Return the entire state by default.\n     * @param {HostElement} host\n     * @param {Object} state\n     * @returns {Object} props\n     */\n    stateToEmbeddedProps(host, state) {\n        const props = this.config.stateToEmbeddedProps?.(host, state) || state;\n        // Clean undefined values to save space\n        for (const key of Object.keys(props)) {\n            if (props[key] === undefined) {\n                delete props[key];\n            }\n        }\n        return props;\n    }\n}\n\n/**\n * Manage updates to `data-embedded-props` (To change props given to an\n * embedded component when it will be mounted in the future), through history\n * and collaborative operations.\n * This is done through a special `embeddedState` which can be used externally\n * as a normal state.\n * That state can be modified through 2 channels:\n * - By the component itself, as with any normal state.\n * - By the embedded_component_plugin, during history or collaborative\n *   operations (undo/redo/resetStepsUntil/addExternalStep). The attribute\n *   `data-embedded-state` will be used to contain a serialized representation\n *   of a state change.\n *\n * While the embedded state evolves, the `data-embedded-props` attribute is\n * always maintained to its relative value.\n *\n * `data-embedded-state` and `data-embedded-props` attributes are maintained\n * even if the related component is in a destroyed state, in order to prepare\n * the next mount operation if the host is re-inserted in the DOM through an\n * history operation.\n * If the component is currently mounted/being mounted, state changes are\n * applied to the attribute and the embeddedState object.\n *\n * By default, a property change in the state is handled by replacing the\n * previous value with the new one (overwrite). To change this behavior,\n * provide a config extension in `getStateChangeManager` in the embedding\n * definition, with a @see propertyUpdater mapping each state key to a change\n * handler function.\n *\n * @param {HostElement} host\n * @returns {Proxy} embeddedState state which can be used for rendering, and\n *                  which is tied to the saved embedded props. Can only contain\n *                  JSON serializable values.\n */\nexport function useEmbeddedState(host) {\n    const component = useComponent();\n    if (!component.env.getStateChangeManager) {\n        throw new Error(\n            \"Missing `getStateChangeManager` function in the `embedding` provided to the `EmbeddedComponentPlugin`.\"\n        );\n    }\n    const stateChangeManager = component.env.getStateChangeManager(host);\n    onWillDestroy(() => stateChangeManager.setupUnmounted());\n    const state = useState(stateChangeManager.getEmbeddedState());\n    return stateChangeManager.constructEmbeddedState(state);\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useForwardRefToParent } from \"@web/core/utils/hooks\";\n\nexport class EmbeddedComponentToolbar extends Component {\n    static props = {\n        buttonsGroupClass: { type: String, optional: true },\n        slots: Object,\n    };\n    static template = \"html_editor.EmbeddedComponentToolbar\";\n}\n\nexport class EmbeddedComponentToolbarButton extends Component {\n    static props = {\n        buttonRef: { type: Function, optional: true },\n        hidden: { type: Boolean, optional: true },\n        icon: { type: String, optional: true },\n        label: String,\n        name: { type: String, optional: true },\n        onClick: Function,\n        title: { type: String, optional: true },\n    };\n    static template = \"html_editor.EmbeddedComponentToolbarButton\";\n\n    setup() {\n        useForwardRefToParent(\"buttonRef\");\n    }\n}\n", "import { useDebounced } from \"@web/core/utils/timing\";\nimport { onWillUnmount, useComponent } from \"@odoo/owl\";\n\n/**\n * This hook can be used to setup temporary mouse events. The returned callback\n * can be passed as a handler for `mousedown` event on a component template.\n * Typical use would be resizing: mousedown on a handle, move the mouse to\n * adapt dimensions, mouseup when finished.\n * @TODO engagement: handle scroll events?\n *\n * @param {Object} options\n * @param {Function} [options.onMouseDown]\n * @param {Function} [options.onMouseMove]\n * @param {Function} [options.onMouseUp]\n * @returns {Function} callback to apply on `mousedown` on a template element\n */\nexport function useMouseResizeListeners(options) {\n    const component = useComponent();\n    options.onMouseUp = (options.onMouseUp || (() => {})).bind(component);\n    options.onMouseDown = (options.onMouseDown || (() => {})).bind(component);\n    const onMouseMove = useDebounced(options.onMouseMove || (() => {}), \"animationFrame\");\n    const onMouseUp = (event) => {\n        document.removeEventListener(\"mousemove\", onMouseMove);\n        onMouseMove.cancel(true);\n        options.onMouseUp(event);\n    };\n    onWillUnmount(() => {\n        document.removeEventListener(\"mousemove\", onMouseMove);\n        document.removeEventListener(\"mouseup\", onMouseUp);\n    });\n    return (event) => {\n        options.onMouseDown(event);\n        document.addEventListener(\"mousemove\", onMouseMove);\n        document.addEventListener(\"mouseup\", onMouseUp, { once: true });\n    };\n}\n", "import { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { getEmbeddedProps } from \"@html_editor/others/embedded_component_utils\";\nimport { checkURL, excalidrawWebsiteDomainList } from \"@html_editor/utils/url\";\nimport { Component, onWillStart, useRef, useState } from \"@odoo/owl\";\nimport { useMouseResizeListeners } from \"@html_editor/others/embedded_components/core/excalidraw/excalidraw_utils\";\n\n/**\n * This Behavior loads an Excalidraw iframe to grant users the ability to present schematics and\n * slides.\n */\nexport class ReadonlyEmbeddedExcalidrawComponent extends Component {\n    static template = \"html_editor.ReadonlyEmbeddedExcalidraw\";\n    static props = {\n        height: { type: String, optional: true },\n        source: { type: String },\n        width: { type: String, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.isMobile = isMobileOS();\n        this.state = useState({\n            height: this.props.height || \"400px\",\n            source: this.props.source,\n            width: this.isMobile ? \"100%\" : this.props.width || \"100%\",\n        });\n        this.displayState = useState({\n            hasError: false,\n            isResizing: false,\n        });\n        this.drawContainer = useRef(\"drawContainer\");\n\n        onWillStart(() => this.setupIframe());\n\n        this.onHandleMouseDown = useMouseResizeListeners({\n            onMouseDown: this.onMouseDown,\n            onMouseMove: this.onMouseMove,\n            onMouseUp: this.onMouseUp,\n        });\n    }\n\n    get templateState() {\n        return this.state;\n    }\n\n    setupIframe() {\n        const url = checkURL(this.props.source, excalidrawWebsiteDomainList);\n        if (url) {\n            this.setURL(url);\n        } else {\n            this.displayState.hasError = true;\n        }\n    }\n\n    setURL(url) {\n        this.state.source = url;\n    }\n\n    onMouseDown() {\n        if (!this.drawContainer.el) {\n            return;\n        }\n        this.displayState.isResizing = true;\n        const bounds = this.drawContainer.el.getBoundingClientRect();\n        this.refPoint = {\n            x: bounds.x + bounds.width / 2,\n            y: bounds.y,\n        };\n    }\n\n    onMouseMove(event) {\n        event.preventDefault();\n        this.state.width = this.isMobile\n            ? this.state.width\n            : `${Math.round(Math.max(2 * Math.abs(this.refPoint.x - event.clientX), 300))}px`;\n        this.state.height = `${Math.round(Math.max(event.clientY - this.refPoint.y, 300))}px`;\n    }\n\n    onMouseUp() {\n        this.displayState.isResizing = false;\n    }\n}\n\nexport const readonlyExcalidrawEmbedding = {\n    name: \"draw\",\n    Component: ReadonlyEmbeddedExcalidrawComponent,\n    getProps: (host) => {\n        return { ...getEmbeddedProps(host) };\n    },\n};\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { downloadFile } from \"@web/core/network/download\";\nimport { useFileViewer } from \"@web/core/file_viewer/file_viewer_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport {\n    EmbeddedComponentToolbar,\n    EmbeddedComponentToolbarButton,\n} from \"@html_editor/others/embedded_components/core/embedded_component_toolbar/embedded_component_toolbar\";\nimport { StateFileModel } from \"@html_editor/others/embedded_components/core/file/state_file_model\";\nimport { getEmbeddedProps } from \"@html_editor/others/embedded_component_utils\";\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class ReadonlyEmbeddedFileComponent extends Component {\n    static components = {\n        EmbeddedComponentToolbar,\n        EmbeddedComponentToolbarButton,\n    };\n    static props = {\n        fileData: { type: Object },\n        host: { type: Object },\n    };\n    static template = \"html_editor.ReadonlyEmbeddedFile\";\n\n    setup() {\n        this.dialogService = useService(\"dialog\");\n        this.state = useState({\n            fileData: { ...this.props.fileData },\n        });\n        this.fileModel = new StateFileModel(this.state);\n        this.attachmentViewer = useFileViewer();\n    }\n\n    /**\n     * Callback function called when the user clicks on the \"Download\" button.\n     * The function will simply open a link that will trigger the download of\n     * the associated file. If the url is not valid, the function will display\n     * an error message.\n     * @param {Event} ev\n     */\n    async onClickDownload(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        try {\n            await downloadFile(this.fileModel.downloadUrl);\n        } catch {\n            this.dialogService.add(AlertDialog, {\n                body: _t(\n                    \"Oops, the file %s could not be found. Please replace this file box by a new one to re-upload the file.\",\n                    this.fileModel.name\n                ),\n                title: _t(\"Missing File\"),\n                confirm: () => {},\n                confirmLabel: _t(\"Close\"),\n            });\n        }\n    }\n\n    onClickFileImage() {\n        if (this.fileModel.isViewable) {\n            this.attachmentViewer.open(this.fileModel);\n        }\n    }\n}\n\nexport const readonlyFileEmbedding = {\n    name: \"file\",\n    Component: ReadonlyEmbeddedFileComponent,\n    getProps: (host) => {\n        return { host, ...getEmbeddedProps(host) };\n    },\n};\n", "import { FileModel } from \"@web/core/file_viewer/file_model\";\n\nexport class StateFileModel extends FileModel {\n    constructor(state) {\n        super();\n        this.state = state;\n        for (const property of [\n            \"access_token\",\n            \"checksum\",\n            \"extension\",\n            \"filename\",\n            \"id\",\n            \"mimetype\",\n            \"name\",\n            \"type\",\n            \"tmpUrl\",\n            \"url\",\n            \"uploading\",\n        ]) {\n            Object.defineProperty(this, property, {\n                get() {\n                    return this.state.fileData[property];\n                },\n                set(value) {\n                    this.state.fileData[property] = value;\n                },\n                configurable: true,\n                enumerable: true,\n            });\n        }\n    }\n}\n", "import { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { TableOfContentManager } from \"@html_editor/others/embedded_components/core/table_of_content/table_of_content_manager\";\n\nexport class EmbeddedTableOfContentComponent extends Component {\n    static template = \"html_editor.EmbeddedTableOfContent\";\n    static props = {\n        manager: { type: TableOfContentManager },\n        readonly: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.state = useState({ toc: this.props.manager.structure });\n        onWillStart(async () => {\n            await this.props.manager.batchedUpdateStructure();\n        });\n    }\n\n    displayTocHint() {\n        return this.state.toc.headings.length < 2 && !this.props.readonly;\n    }\n\n    /**\n     * @param {Object} heading\n     */\n    onTocLinkClick(heading) {\n        this.props.manager.scrollIntoView(heading);\n    }\n}\n\nexport const tableOfContentEmbedding = {\n    name: \"tableOfContent\",\n    Component: EmbeddedTableOfContentComponent,\n};\n\nexport const readonlyTableOfContentEmbedding = {\n    name: \"tableOfContent\",\n    Component: EmbeddedTableOfContentComponent,\n    getProps: (host) => {\n        return {\n            readonly: true,\n        };\n    },\n};\n", "import { batched, reactive } from \"@odoo/owl\";\n\nexport const HEADINGS = [\"H1\", \"H2\", \"H3\", \"H4\", \"H5\", \"H6\"];\n\nexport class TableOfContentManager {\n    constructor(containerRef) {\n        this.containerRef = containerRef;\n        this.structure = reactive({\n            headings: [],\n        });\n        this.batchedUpdateStructure = batched(this.updateStructure.bind(this));\n    }\n\n    getContainerEl() {\n        return this.containerRef.el;\n    }\n\n    /**\n     * Allows to fetch relevant headings in the page when building the Table of Content.\n     * Will filter out things we don't want:\n     * - Empty headers\n     * - Headers only containing the 'ZeroWidthSpace' element ('\\u200B')\n     * - Headers descendants of an element with `data-embedded`\n     *\n     * @param {Element} element\n     */\n    fetchValidHeadings(element) {\n        const inEmbeddedHeadings = new Set(\n            element.querySelectorAll(\n                HEADINGS.map((heading) => `[data-embedded] ${heading}`).join(\",\")\n            )\n        );\n        return Array.from(element.querySelectorAll(HEADINGS.join(\",\")))\n            .filter((heading) => heading.innerText.trim().replaceAll(\"\\u200B\", \"\").length > 0)\n            .filter((heading) => !inEmbeddedHeadings.has(heading));\n    }\n\n    scrollIntoView(heading) {\n        if (!heading) {\n            return;\n        }\n        const { target } = heading;\n        target.scrollIntoView({ behavior: \"smooth\" });\n        target.classList.add(\"o_embedded_toc_header_highlight\");\n        window.setTimeout(() => {\n            target.classList.remove(\"o_embedded_toc_header_highlight\");\n        }, 2000);\n    }\n\n    updateStructure() {\n        let currentDepthByTag = {};\n        let previousTag;\n        let previousDepth = -1;\n        const container = this.getContainerEl();\n        if (!container) {\n            return;\n        }\n        this.structure.headings = this.fetchValidHeadings(container).map((heading) => {\n            let depth = HEADINGS.indexOf(heading.tagName);\n            if (depth !== previousDepth && heading.tagName === previousTag) {\n                depth = previousDepth;\n            } else if (depth > previousDepth) {\n                if (heading.tagName !== previousTag && HEADINGS.indexOf(previousTag) < depth) {\n                    depth = previousDepth + 1;\n                } else {\n                    depth = previousDepth;\n                }\n            } else if (depth < previousDepth) {\n                if (currentDepthByTag.hasOwnProperty(heading.tagName)) {\n                    depth = currentDepthByTag[heading.tagName];\n                }\n            }\n\n            previousTag = heading.tagName;\n            previousDepth = depth;\n\n            // going back to 0 depth, wipe-out the 'currentDepthByTag'\n            if (depth === 0) {\n                currentDepthByTag = {};\n            }\n            currentDepthByTag[heading.tagName] = depth;\n\n            return {\n                depth: depth,\n                name: heading.innerText,\n                target: heading,\n            };\n        });\n    }\n}\n", "import { getEmbeddedProps } from \"@html_editor/others/embedded_component_utils\";\nimport { getVideoUrl } from \"@html_editor/utils/url\";\nimport { Component } from \"@odoo/owl\";\n\nexport class EmbeddedVideoIframe extends Component {\n    static template = \"html_editor.EmbeddedVideoIframe\";\n    static props = {\n        src: { type: String },\n    };\n}\n\nexport class EmbeddedVideoComponent extends Component {\n    static template = \"html_editor.EmbeddedVideo\";\n    static props = {\n        platform: { type: String },\n        videoId: { type: String },\n        params: { type: Object, optional: true },\n    };\n    static components = { VideoIframe: EmbeddedVideoIframe };\n\n    setup() {\n        super.setup();\n        const url = getVideoUrl(this.props.platform, this.props.videoId, this.props.params);\n        this.src = url.toString();\n    }\n}\n\nexport const videoEmbedding = {\n    name: \"video\",\n    Component: EmbeddedVideoComponent,\n    getProps: (host) => {\n        return { ...getEmbeddedProps(host) };\n    },\n};\n", "import { Component } from \"@odoo/owl\";\n\nexport class ArticleIndexList extends Component {\n    static template = \"knowledge.ArticleIndexList\";\n    static props = {\n        articles: { type: Object },\n    };\n\n    onArticleLinkClick(ev, articleId) {\n        if (this.env.openArticle) {\n            ev.preventDefault();\n            ev.stopPropagation();\n            this.env.openArticle(articleId);\n        }\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { getEmbeddedProps } from \"@html_editor/others/embedded_component_utils\";\nimport { ArticleIndexList } from \"@knowledge/editor/embedded_components/core/article_index/article_index_list\";\n\nexport class ReadonlyEmbeddedArticleIndexComponent extends Component {\n    static props = {\n        articles: { type: Object, optional: true },\n        showAllChildren: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        showAllChildren: true,\n    };\n    static template = \"knowledge.ReadonlyEmbeddedArticleIndex\";\n    static components = { ArticleIndexList };\n}\n\nexport const readonlyArticleIndexEmbedding = {\n    name: \"articleIndex\",\n    Component: ReadonlyEmbeddedArticleIndexComponent,\n    getProps: (host) => {\n        return {\n            ...getEmbeddedProps(host),\n        };\n    },\n};\n", "import {\n    getEditableDescendants,\n    useEditableDescendants,\n} from \"@html_editor/others/embedded_component_utils\";\nimport {\n    EmbeddedComponentToolbar,\n    EmbeddedComponentToolbarButton,\n} from \"@html_editor/others/embedded_components/core/embedded_component_toolbar/embedded_component_toolbar\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Tooltip } from \"@web/core/tooltip/tooltip\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\n\nexport class EmbeddedClipboardComponent extends Component {\n    static components = {\n        EmbeddedComponentToolbar,\n        EmbeddedComponentToolbarButton,\n    };\n    static props = {\n        host: { type: Object },\n    };\n    static template = \"knowledge.EmbeddedClipboard\";\n\n    setup() {\n        this.popover = usePopover(Tooltip);\n        this.descendants = useEditableDescendants(this.props.host);\n        this.copyToClipboardButtonRef = useChildRef();\n    }\n\n    //--------------------------------------------------------------------------\n    // HANDLERS\n    //--------------------------------------------------------------------------\n\n    async onClickCopyToClipboard() {\n        const selection = document.getSelection();\n        selection.removeAllRanges();\n        const range = new Range();\n        range.selectNodeContents(this.descendants.clipboardContent);\n        selection.addRange(range);\n        if (document.execCommand(\"copy\")) {\n            // Nor the original `clipboard.write` function nor the polyfill\n            // written in `clipboard.js` does trigger the `clipboard_plugin`\n            // `copy` handler, therefore `execCommand` should be called here so\n            // that html content is properly handled within the editor.\n            this.popover.open(this.copyToClipboardButtonRef.el, {\n                tooltip: _t(\"Content copied to clipboard.\"),\n            });\n            browser.setTimeout(this.popover.close, 800);\n        }\n        selection.removeAllRanges();\n    }\n}\n\nexport const clipboardEmbedding = {\n    name: \"clipboard\",\n    Component: EmbeddedClipboardComponent,\n    getEditableDescendants: getEditableDescendants,\n    getProps: (host) => {\n        return { host };\n    },\n};\n", "import { _t } from \"@web/core/l10n/translation\";\n\nexport const EMBEDDED_VIEW_LINK_STYLES = {\n    link: { display: _t(\"Link\"), class: \"btn btn-link\" },\n    primary: { display: _t(\"Primary\"), class: \"btn btn-primary\" },\n    secondary: { display: _t(\"Secondary\"), class: \"btn btn-secondary\" },\n};\n", "import { HtmlViewer } from \"@html_editor/fields/html_viewer\";\nimport { HtmlUpgradeManager } from \"@knowledge/editor/html_migrations/html_upgrade_manager\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(HtmlViewer.prototype, {\n    setup() {\n        this.htmlUpgradeManager = this.env.htmlUpgradeManager || new HtmlUpgradeManager();\n        // super setup is called after because it uses formatValue\n        super.setup();\n    },\n\n    formatValue(value) {\n        const current = super.formatValue(value);\n        return this.htmlUpgradeManager.processForUpgrade(current);\n    },\n});\n", "import {\n    compareVersions,\n    VERSION_SELECTOR,\n    htmlEditorVersions,\n} from \"@knowledge/editor/html_migrations/manifest\";\nimport { registry } from \"@web/core/registry\";\nimport { markup } from \"@odoo/owl\";\n\nexport class HtmlUpgradeManager {\n    constructor(env = {}) {\n        this.upgradeRegistry = registry.category(\"html_editor_upgrade\");\n        this.parser = new DOMParser();\n        this.originalValue = undefined;\n        this.upgradedValue = undefined;\n        this.body = undefined;\n        this.env = env;\n    }\n\n    get value() {\n        if (this.originalValue?.constructor?.name === \"Markup\") {\n            return markup(this.upgradedValue);\n        }\n        return this.upgradedValue;\n    }\n\n    processForUpgrade(value) {\n        const strValue = value.toString();\n        if (\n            strValue === this.originalValue?.toString() ||\n            strValue === this.upgradedValue?.toString()\n        ) {\n            return this.value;\n        }\n        this.originalValue = value;\n        this.upgradedValue = value;\n        this.body = this.parser.parseFromString(value.toString(), \"text/html\").body;\n        const versionNode = this.body.querySelector(VERSION_SELECTOR);\n        const version = versionNode?.dataset.oeVersion || \"0.0\";\n        const VERSIONS = htmlEditorVersions();\n        const currentVersion = VERSIONS.at(-1);\n        if (!currentVersion || version === currentVersion) {\n            return this.value;\n        }\n        try {\n            const upgradeSequence = VERSIONS.filter((subVersion) => {\n                // skip already applied versions\n                return compareVersions(subVersion, version) > 0;\n            });\n            this.upgradedValue = this.upgrade(upgradeSequence);\n        } catch {\n            // If an upgrade fails, silently continue to use the raw value.\n        }\n        return this.value;\n    }\n\n    upgrade(upgradeSequence) {\n        for (const version of upgradeSequence) {\n            const modules = this.upgradeRegistry.category(version);\n            for (const [key, module] of modules.getEntries()) {\n                const upgrade = odoo.loader.modules.get(module).upgrade;\n                if (!upgrade) {\n                    console.error(\n                        `An \"${key}\" upgrade function could not be found at \"${module}\" or it did not load.`\n                    );\n                }\n                upgrade(this.body, this.env);\n            }\n        }\n        return this.body.innerHTML;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\n\nconst upgradeRegistry = registry.category(\"html_editor_upgrade\");\n\nupgradeRegistry.category(\"1.0\").add(\"knowledge\", \"@knowledge/editor/html_migrations/migration-1.0\");\n\nexport function htmlEditorVersions() {\n    return Object.keys(upgradeRegistry.subRegistries).sort(compareVersions);\n}\n\nexport const VERSION_SELECTOR = \"[data-oe-version]\";\n\nexport function stripVersion(element) {\n    element.querySelectorAll(VERSION_SELECTOR).forEach((el) => {\n        delete el.dataset.oeVersion;\n    });\n}\n\n/**\n * Compare 2 versions\n *\n * @param {string} version1\n * @param {string} version2\n * @returns {number} -1 if version1 < version2\n *                   0 if version1 === version2\n *                   1 if version1 > version2\n */\nexport function compareVersions(version1, version2) {\n    version1 = version1.split(\".\").map((v) => parseInt(v));\n    version2 = version2.split(\".\").map((v) => parseInt(v));\n    if (version1[0] < version2[0] || (version1[0] === version2[0] && version1[1] < version2[1])) {\n        return -1;\n    } else if (version1[0] === version2[0] && version1[1] === version2[1]) {\n        return 0;\n    } else {\n        return 1;\n    }\n}\n", "import { decodeDataBehaviorProps, getPropNameNode } from \"@knowledge/editor/html_migrations/utils\";\nimport { getOrigin } from \"@web/core/utils/urls\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport function upgrade(container, env) {\n    for (const [key, selector] of Object.entries(selectors)) {\n        const elements = container.querySelectorAll(selector);\n        if (elements.length) {\n            upgrades[key](elements, env);\n        }\n    }\n}\n\nfunction upgradeSearchModelState(searchModelState) {\n    searchModelState = JSON.parse(searchModelState);\n    const dfOptMap = {\n        this_year: \"year\",\n        last_year: \"year-1\",\n        antepenultimate_year: \"year-2\",\n        this_month: \"month\",\n        last_month: \"month-1\",\n        antepenultimate_month: \"month-2\",\n    };\n    for (const searchItem of Object.values(searchModelState.searchItems)) {\n        if (searchItem.type === \"dateFilter\") {\n            const newDefaults = new Set();\n            for (const generatorId of searchItem.defaultGeneratorIds) {\n                if (generatorId in dfOptMap) {\n                    newDefaults.add(dfOptMap[generatorId]);\n                }\n            }\n            if (newDefaults.size) {\n                searchItem.defaultGeneratorIds = Array.from(newDefaults);\n            }\n            if (!searchItem.optionsParams) {\n                searchItem.optionsParams = {\n                    startYear: -2,\n                    endYear: 0,\n                    startMonth: -2,\n                    endMonth: 0,\n                    customOptions: [],\n                };\n            }\n            for (const queryItem of searchModelState.query) {\n                if (\n                    queryItem.searchItemId === searchItem.id &&\n                    queryItem.generatorId &&\n                    queryItem.generatorId in dfOptMap\n                ) {\n                    queryItem.generatorId = dfOptMap[queryItem.generatorId];\n                }\n            }\n        }\n    }\n    return JSON.stringify(searchModelState);\n}\n\nconst selectors = {\n    articleBehavior: \".o_knowledge_behavior_type_article\",\n    articlesStructureBehavior: \".o_knowledge_behavior_type_articles_structure\",\n    comments: \".knowledge-thread-comment\",\n    drawBehavior: \".o_knowledge_behavior_type_draw\",\n    embeddedViewBehavior: \".o_knowledge_behavior_type_embedded_view\",\n    fileBehavior: \".o_knowledge_behavior_type_file\",\n    tableOfContentBehavior: \".o_knowledge_behavior_type_toc\",\n    templateBehavior: \".o_knowledge_behavior_type_template\",\n    videoBehavior: \".o_knowledge_behavior_type_video\",\n    viewLinkBehavior: \".o_knowledge_behavior_type_view_link\",\n};\n\nconst upgrades = {\n    articleBehavior: (elements) => {\n        for (const el of elements) {\n            const oldProps = decodeDataBehaviorProps(el.dataset.behaviorProps);\n            if (!oldProps?.article_id || !oldProps?.display_name) {\n                // Abort the conversion if data can not be recovered.\n                // Element will still exist in the DOM as raw data.\n                continue;\n            }\n            delete el.dataset.behaviorProps;\n            el.dataset.res_id = oldProps?.article_id;\n            el.removeAttribute(\"tabindex\");\n            el.classList.remove(\"o_knowledge_behavior_anchor\", \"o_knowledge_behavior_type_article\");\n            el.classList.add(\"o_knowledge_article_link\");\n            el.replaceChildren(document.createTextNode(oldProps.display_name));\n        }\n    },\n    articlesStructureBehavior: (elements) => {\n        function buildIndex(el, props) {\n            const index = [];\n            while (el) {\n                const anchor = el.querySelector(\"a\");\n                const id = parseInt(\n                    anchor\n                        .getAttribute(\"href\")\n                        .match(/(\\d+)$/)\n                        .at(1)\n                );\n                const name = anchor.textContent;\n                const article = { id, name, childIds: [] };\n                el = el.nextElementSibling;\n                const child = el?.querySelector(\":scope > ol > li\");\n                if (child) {\n                    article.childIds = buildIndex(child, props);\n                    props.showAllChildren = true;\n                    el = el.nextElementSibling;\n                }\n                index.push(article);\n            }\n            return index;\n        }\n        for (const el of elements) {\n            const props = {};\n            try {\n                const content = getPropNameNode(\"content\", el);\n                const articles = buildIndex(content.querySelector(\"li\"), props);\n                if (articles.length) {\n                    props.articles = articles;\n                    el.dataset.embeddedProps = JSON.stringify(props);\n                }\n            } catch {\n                // ignore the existing article index if the parsing fails, it will\n                // have to be refreshed manually.\n            }\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            el.dataset.embedded = \"articleIndex\";\n            el.replaceChildren();\n        }\n    },\n    comments: (elements, env) => {\n        function createBeacon({ type, threadId, resId, resModel, disabled }) {\n            const anchor = document.createElement(\"A\");\n            anchor.classList.add(\"oe_unremovable\", \"oe_thread_beacon\");\n            anchor.dataset.id = threadId;\n            anchor.dataset.res_id = resId;\n            anchor.dataset.resModel = resModel;\n            anchor.dataset.oeType = type;\n            if (disabled) {\n                anchor.classList.add(\"oe_disabled_thread_beacon\");\n            }\n            return anchor;\n        }\n        function createBeacons({ anchors, threadId, resId, resModel }) {\n            const start = anchors.at(0);\n            const end = anchors.at(-1);\n            if (!start || !end) {\n                return;\n            }\n            const disabled = !start.classList.contains(\"knowledge-thread-highlighted-comment\");\n            const beaconStart = createBeacon({\n                type: \"threadBeaconStart\",\n                threadId,\n                resId,\n                resModel,\n                disabled,\n            });\n            const beaconEnd = createBeacon({\n                type: \"threadBeaconEnd\",\n                threadId,\n                resId,\n                resModel,\n                disabled,\n            });\n            start.before(beaconStart);\n            end.after(beaconEnd);\n        }\n        function groupComments(anchors) {\n            const comments = {};\n            for (const anchor of anchors) {\n                const threadId = anchor.dataset.id;\n                if (!threadId) {\n                    continue;\n                }\n                comments[threadId] ||= [];\n                comments[threadId].push(anchor);\n            }\n            return comments;\n        }\n        const resId = env.model?.root?.resId;\n        const resModel = env.model?.root?.resModel;\n        if (resId && resModel) {\n            // Only create new comment beacons if the env has a record.\n            const comments = groupComments(elements);\n            for (const threadId in comments) {\n                createBeacons({ anchors: comments[threadId], threadId, resId, resModel });\n            }\n        }\n        // Remove old comments anchors (not a big deal if there is no replacement for some)\n        for (const el of [...elements]) {\n            if (el.nodeName === \"SPAN\") {\n                const childNodes = [];\n                while (el.firstChild) {\n                    childNodes.push(el.firstChild);\n                    el.firstChild.remove();\n                }\n                el.replaceWith(...childNodes);\n                continue;\n            }\n            el.classList.remove(\n                \"focused-comment\",\n                \"knowledge-thread-highlighted-comment\",\n                \"knowledge-thread-comment\"\n            );\n            delete el.dataset.id;\n            el.removeAttribute(\"tabindex\");\n        }\n    },\n    drawBehavior: (elements) => {\n        for (const el of elements) {\n            const oldProps = decodeDataBehaviorProps(el.dataset.behaviorProps);\n            if (!oldProps || !oldProps.source) {\n                // Abort the conversion if data can not be recovered.\n                // Element will still exist in the DOM as raw data.\n                continue;\n            }\n            const props = {\n                height: oldProps.height,\n                source: oldProps.source,\n                width: oldProps.width,\n            };\n            el.dataset.embedded = \"draw\";\n            el.dataset.embeddedProps = JSON.stringify(props);\n            delete el.dataset.behaviorProps;\n            delete el.dataset.oeTransientContent;\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            el.replaceChildren();\n        }\n    },\n    embeddedViewBehavior: (elements) => {\n        for (const el of elements) {\n            const oldProps = decodeDataBehaviorProps(el.dataset.behaviorProps);\n            const viewProps = {\n                context: oldProps.context,\n                displayName: oldProps.display_name,\n                favoriteFilters: {},\n                id: oldProps.embedded_view_id,\n                viewType: oldProps.view_type,\n            };\n            if (oldProps.act_window) {\n                viewProps.actWindow = oldProps.act_window;\n            } else {\n                viewProps.actionXmlId = oldProps.action_xml_id;\n            }\n            if (oldProps.additionalViewProps) {\n                viewProps.additionalViewProps = oldProps.additionalViewProps;\n            }\n            if (oldProps.favorites) {\n                // favorites was an array, is now an object\n                for (const filter of oldProps.favorites) {\n                    viewProps.favoriteFilters[filter.name] = filter;\n                }\n            }\n            if (viewProps.context.knowledge_search_model_state) {\n                viewProps.context.knowledge_search_model_state = upgradeSearchModelState(\n                    viewProps.context.knowledge_search_model_state\n                );\n            }\n            delete el.dataset.behaviorProps;\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            el.dataset.embedded = \"view\";\n            el.dataset.embeddedProps = JSON.stringify({ viewProps });\n            el.replaceChildren();\n        }\n    },\n    fileBehavior: (elements) => {\n        for (const el of elements) {\n            const oldProps = decodeDataBehaviorProps(el.dataset.behaviorProps);\n            const htmlFileName = getPropNameNode(\"fileName\", el)?.textContent;\n            const htmlFileExtension = getPropNameNode(\"fileExtension\", el)?.textContent;\n            const htmlFileImageLink = getPropNameNode(\"fileImage\", el)?.querySelector(\"a\");\n            const href = htmlFileImageLink?.getAttribute(\"href\");\n            const mimetype = htmlFileImageLink?.dataset.mimetype;\n            let accessToken, checksum, id, type, url;\n            if (href?.startsWith(getOrigin())) {\n                id = parseInt((href.match(/\\/web\\/(?:content|image)\\/(\\d+)/) || [])[1]);\n                checksum = (href.match(/unique=([^&]+)/) || [])[1];\n                accessToken = (href.match(/access_token=([^&]+)/) || [])[1];\n            }\n            if (!id) {\n                type = \"url\";\n                url = href?.replace(/\\?.*$/, \"\");\n            } else {\n                type = \"binary\";\n            }\n            const fileName = htmlFileName || oldProps?.fileName || _t(\"Untitled\");\n            const extension = htmlFileExtension || oldProps?.fileExtension;\n            let fileData = oldProps?.fileData;\n            // accessToken has been renamed in file_model\n            if (fileData?.accessToken) {\n                fileData.access_token = fileData.accessToken;\n                delete fileData.accessToken;\n            }\n            if (!id && !url && !fileData) {\n                // Abort the conversion if data can not be recovered.\n                // Element will still exist in the DOM as raw data.\n                continue;\n            }\n            if (!fileData) {\n                fileData = {\n                    access_token: accessToken,\n                    checksum,\n                    extension,\n                    filename: fileName,\n                    id,\n                    mimetype,\n                    name: fileName,\n                    type,\n                    url,\n                };\n            }\n            const props = {\n                fileData,\n            };\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            delete el.dataset.behaviorProps;\n            el.dataset.embedded = \"file\";\n            el.dataset.embeddedProps = JSON.stringify(props);\n            el.replaceChildren();\n        }\n    },\n    tableOfContentBehavior: (elements) => {\n        for (const el of elements) {\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            el.dataset.embedded = \"tableOfContent\";\n            el.replaceChildren();\n        }\n    },\n    templateBehavior: (elements) => {\n        for (const el of elements) {\n            let content = getPropNameNode(\"content\", el);\n            if (!content) {\n                content = document.createElement(\"DIV\");\n                const p = document.createElement(\"P\");\n                const br = document.createElement(\"BR\");\n                p.append(br);\n                content.append(p);\n            }\n            content.removeAttribute(\"class\");\n            delete content.dataset.propName;\n            content.dataset.embeddedEditable = \"clipboardContent\";\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            el.dataset.embedded = \"clipboard\";\n            el.replaceChildren(content);\n        }\n    },\n    videoBehavior: (elements) => {\n        for (const el of elements) {\n            const oldProps = decodeDataBehaviorProps(el.dataset.behaviorProps);\n            if (!oldProps || !oldProps.platform || !oldProps.videoId) {\n                // Abort the conversion if data can not be recovered.\n                // Element will still exist in the DOM as raw data.\n                continue;\n            }\n            delete el.dataset.behaviorProps;\n            const props = {\n                platform: oldProps.platform,\n                videoId: oldProps.videoId,\n                params: oldProps.params,\n            };\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            el.dataset.embedded = \"video\";\n            el.dataset.embeddedProps = JSON.stringify(props);\n            el.replaceChildren();\n        }\n    },\n    viewLinkBehavior: (elements) => {\n        for (const el of elements) {\n            const oldProps = decodeDataBehaviorProps(el.dataset.behaviorProps);\n            const viewProps = {\n                context: oldProps.context,\n                displayName: oldProps.name,\n                viewType: oldProps.view_type,\n            };\n            const props = {};\n            if (oldProps.style) {\n                props.linkStyle = oldProps.style;\n            }\n            if (oldProps.act_window) {\n                viewProps.actWindow = oldProps.act_window;\n            } else {\n                viewProps.actionXmlId = oldProps.action_xml_id;\n            }\n            if (viewProps.context.knowledge_search_model_state) {\n                viewProps.context.knowledge_search_model_state = upgradeSearchModelState(\n                    viewProps.context.knowledge_search_model_state\n                );\n            }\n            delete el.dataset.behaviorProps;\n            el.removeAttribute(\"class\");\n            el.removeAttribute(\"tabindex\");\n            el.dataset.embedded = \"viewLink\";\n            el.dataset.embeddedProps = JSON.stringify({ viewProps, ...props });\n            el.replaceChildren();\n        }\n    },\n};\n", "/**\n * Convert the string from a data-behavior-props attribute to an usable object.\n *\n * @param {String} dataBehaviorPropsAttribute utf-8 encoded JSON string\n * @returns {Object} object containing props for a Behavior to store in the\n *                   html_field value of a field\n */\nexport function decodeDataBehaviorProps(dataBehaviorPropsAttribute) {\n    if (!dataBehaviorPropsAttribute) {\n        return undefined;\n    }\n    return JSON.parse(decodeURIComponent(dataBehaviorPropsAttribute));\n}\n\n/**\n * Return any existing propName node owned by the Behavior related to `anchor`.\n * Filter out propName nodes owned by children Behavior.\n *\n * @param {string} propName name of the htmlProp\n * @param {Element} anchor node to search for propName children\n * @returns {Element} last matching node (there should be only one, but it's\n *           always the last one that is taken as the effective prop)\n */\nexport function getPropNameNode(propName, anchor) {\n    const propNodes = anchor.querySelectorAll(`[data-prop-name=\"${propName}\"]`);\n    for (let i = propNodes.length - 1; i >= 0; i--) {\n        const closest = propNodes[i].closest(\".o_knowledge_behavior_anchor\");\n        if (closest === anchor) {\n            return propNodes[i];\n        }\n    }\n}\n\n/**\n * Generate a unique identifier (64 bits) in hexadecimal.\n *\n * @returns {string}\n */\nexport function uuid() {\n    const array = new Uint8Array(8);\n    window.crypto.getRandomValues(array);\n    // Uint8Array to hex\n    return [...array].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n", "import { readonlyExcalidrawEmbedding } from \"@html_editor/others/embedded_components/core/excalidraw/readonly_excalidraw\";\nimport { readonlyFileEmbedding } from \"@html_editor/others/embedded_components/core/file/readonly_file\";\nimport { readonlyTableOfContentEmbedding } from \"@html_editor/others/embedded_components/core/table_of_content/table_of_content\";\nimport { videoEmbedding } from \"@html_editor/others/embedded_components/core/video/video\";\nimport { readonlyArticleIndexEmbedding } from \"@knowledge/editor/embedded_components/core/article_index/readonly_article_index\";\nimport { clipboardEmbedding } from \"@knowledge/editor/embedded_components/core/clipboard/embedded_clipboard\";\nimport { viewPlaceholderEmbedding } from \"@website_knowledge/frontend/editor/embedded_components/view/view_placeholder\";\nimport { publicViewLinkEmbedding } from \"@website_knowledge/frontend/editor/embedded_components/view_link/public_embedded_view_link\";\n\nexport const KNOWLEDGE_PUBLIC_EMBEDDINGS = [\n    clipboardEmbedding,\n    publicViewLinkEmbedding,\n    readonlyArticleIndexEmbedding,\n    readonlyExcalidrawEmbedding,\n    readonlyFileEmbedding,\n    readonlyTableOfContentEmbedding,\n    videoEmbedding,\n    viewPlaceholderEmbedding,\n];\n", "import { Component } from \"@odoo/owl\";\n\nexport class ViewPlaceholderComponent extends Component {\n    static template = \"website_knowledge.ViewPlaceholder\";\n    static props = {};\n\n    setup() {\n        super.setup();\n        this.url = `/knowledge/article/${this.env.articleId}`;\n    }\n}\n\nexport const viewPlaceholderEmbedding = {\n    name: \"view\",\n    Component: ViewPlaceholderComponent,\n};\n", "import { Component } from \"@odoo/owl\";\nimport { getEmbeddedProps } from \"@html_editor/others/embedded_component_utils\";\nimport { EMBEDDED_VIEW_LINK_STYLES } from \"@knowledge/editor/embedded_components/core/embedded_view_link/embedded_view_link_style\";\n\nexport class PublicEmbeddedViewLinkComponent extends Component {\n    static template = \"knowledge.PublicEmbeddedViewLink\";\n    static props = {\n        viewProps: { type: Object },\n        linkStyle: { type: String, optional: true },\n    };\n    static defaultProps = {\n        linkStyle: \"link\",\n    };\n\n    getLinkClass() {\n        return EMBEDDED_VIEW_LINK_STYLES[this.props.linkStyle].class;\n    }\n}\n\nexport const publicViewLinkEmbedding = {\n    name: \"viewLink\",\n    Component: PublicEmbeddedViewLinkComponent,\n    getProps: (host) => {\n        return { ...getEmbeddedProps(host) };\n    },\n};\n", "import { HtmlViewer } from \"@html_editor/fields/html_viewer\";\n\nexport class PublicHtmlViewer extends HtmlViewer {\n    retargetLink(link) {\n        const href = link?.getAttribute(\"href\") || \"\";\n        if (href.startsWith(\"/knowledge/article\") || href.startsWith(\"/web/login\")) {\n            return;\n        }\n        super.retargetLink(link);\n    }\n}\n", "import {\n    Component,\n    markup,\n    onMounted,\n    onPatched,\n    onWillPatch,\n    onWillUnmount,\n    useRef,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { debounce, throttleForAnimation } from \"@web/core/utils/timing\";\nimport { KNOWLEDGE_PUBLIC_EMBEDDINGS } from \"@website_knowledge/frontend/editor/embedded_components/embedding_sets\";\nimport { PublicHtmlViewer } from \"@website_knowledge/frontend/editor/html_viewer/public_html_viewer\";\n\nclass KnowledgePublic extends Component {\n    static components = {\n        HtmlViewer: PublicHtmlViewer,\n    };\n    static props = {\n        record: { type: Object },\n        showSidebar: { type: Boolean },\n    };\n    static template = \"website_knowledge.knowledgePublic\";\n\n    setup() {\n        this.treeRef = useRef(\"tree\");\n        this.storageKey = \"knowledge.unfolded.ids\";\n        this.unfoldedArticlesIds =\n            localStorage.getItem(this.storageKey)?.split(\";\").map(Number) || [];\n        // Debounce the search articles method to reduce the number of rpcs\n        this.searchArticles = debounce(this.searchArticles, 500);\n        if (this.props.record.resId) {\n            this.props.record.data.body = markup(this.props.record.data.body);\n        }\n        // Sidebar handling TODO @engagement TODO ABD\n        // refactor to use an OWL component instead of backend rendering\n        // -> should rpc for new article data when clicking in the sidebar\n        // instead of reloading everything\n        this.state = useState({\n            tree: undefined,\n            sidebarSize: 300,\n            showAsideMobile: false,\n        });\n        this.keepLastRender = new KeepLast();\n        this.renderTree();\n        this.boundLoadMoreArticles = this.loadMoreArticles.bind(this);\n        this.boundFoldArticle = this.foldArticle.bind(this);\n        useSubEnv({\n            articleId: this.props.record.resId,\n        });\n        onMounted(() => {\n            this.addLoadMoreHandlers();\n            this.addFoldHandlers();\n        });\n        onPatched(() => {\n            this.addLoadMoreHandlers();\n            this.addFoldHandlers();\n        });\n        onWillPatch(() => {\n            this.removeLoadMoreHandlers();\n            this.removeFoldHandlers();\n        });\n        onWillUnmount(() => {\n            this.removeLoadMoreHandlers();\n            this.removeFoldHandlers();\n        });\n    }\n\n    addLoadMoreHandlers() {\n        for (const loadMoreEl of this.treeRef.el?.querySelectorAll(\n            \".o_knowledge_article_load_more\"\n        ) || []) {\n            loadMoreEl.addEventListener(\"click\", this.boundLoadMoreArticles);\n        }\n    }\n\n    removeLoadMoreHandlers() {\n        for (const loadMoreEl of this.treeRef.el?.querySelectorAll(\n            \".o_knowledge_article_load_more\"\n        ) || []) {\n            loadMoreEl.removeEventListener(\"click\", this.boundLoadMoreArticles);\n        }\n    }\n\n    addFoldHandlers(el = undefined) {\n        if (el) {\n            el.querySelector(\".o_article_caret\").addEventListener(\"click\", this.boundFoldArticle);\n            return;\n        }\n        for (const loadMoreEl of this.treeRef.el?.querySelectorAll(\".o_article_caret\") || []) {\n            loadMoreEl.addEventListener(\"click\", this.boundFoldArticle);\n        }\n    }\n\n    removeFoldHandlers() {\n        for (const loadMoreEl of this.treeRef.el?.querySelectorAll(\".o_article_caret\") || []) {\n            loadMoreEl.addEventListener(\"click\", this.boundFoldArticle);\n        }\n    }\n\n    /**\n     * Callback function called when the user clicks on the caret of an article\n     * The function will load the children of the article and append them to the\n     * dom. Then, the id of the unfolded article will be added to the cache.\n     * (see: `_renderTree`).\n     * @param {Event} event\n     */\n    async foldArticle(event) {\n        event.stopPropagation();\n        const buttonEl = event.currentTarget;\n\n        const iconEl = buttonEl.querySelector(\"i\");\n        const liEl = buttonEl.closest(\"li\");\n        const articleId = parseInt(liEl.dataset.articleId);\n        const ulEl = liEl.querySelector(\"ul\");\n        if (iconEl.classList.contains(\"fa-caret-down\")) {\n            ulEl.classList.add(\"d-none\");\n            if (this.unfoldedArticlesIds.indexOf(articleId) !== -1) {\n                this.unfoldedArticlesIds.splice(this.unfoldedArticlesIds.indexOf(articleId), 1);\n            }\n            iconEl.classList.remove(\"fa-caret-down\");\n            iconEl.classList.add(\"fa-caret-right\");\n        } else {\n            if (ulEl) {\n                // Show hidden children\n                ulEl.classList.remove(\"d-none\");\n            } else {\n                let childrenEls;\n                try {\n                    childrenEls = await this.loadChildrenArticles(parseInt(liEl.dataset.articleId));\n                } catch (error) {\n                    // Article is not accessible anymore, remove it from the sidebar\n                    liEl.remove();\n                    throw error;\n                }\n                const newUlEl = document.createElement(\"ul\");\n                childrenEls = new DOMParser().parseFromString(childrenEls, \"text/html\").body\n                    .childNodes;\n                childrenEls.forEach((child) => {\n                    newUlEl.appendChild(child);\n                });\n                this.addFoldHandlers(newUlEl);\n                liEl.appendChild(newUlEl);\n            }\n            if (this.unfoldedArticlesIds.indexOf(articleId) === -1) {\n                this.unfoldedArticlesIds.push(articleId);\n            }\n            iconEl.classList.remove(\"fa-caret-right\");\n            iconEl.classList.add(\"fa-caret-down\");\n        }\n        localStorage.setItem(this.storageKey, this.unfoldedArticlesIds.join(\";\"));\n    }\n\n    getConfig() {\n        // TODO ABD: migration hook\n        const config = {\n            value: this.props.record.data.body,\n            embeddedComponents: [...KNOWLEDGE_PUBLIC_EMBEDDINGS],\n        };\n        return config;\n    }\n\n    async loadMoreArticles(ev) {\n        ev.preventDefault();\n        const rpcParams = {\n            active_article_id: this.props.record.resId || false,\n            parent_id: ev.target.dataset[\"parentId\"] || false,\n            limit: ev.target.dataset[\"limit\"],\n            offset: ev.target.dataset[\"offset\"] || 0,\n        };\n\n        const addedArticles = await rpc(\"/knowledge/public_sidebar/load_more\", rpcParams);\n        const listRoot = ev.target.closest(\"ul\");\n        // remove existing \"Load more\" link\n        ev.target.remove();\n        // remove the 'forced' displayed active article\n        const forcedDisplayedActiveArticle = listRoot.querySelector(\n            \".o_knowledge_article_force_show_active_article\"\n        );\n        if (forcedDisplayedActiveArticle) {\n            forcedDisplayedActiveArticle.remove();\n        }\n        // insert the returned template\n        listRoot.insertAdjacentHTML(\"beforeend\", addedArticles);\n        this.addLoadMoreHandlers();\n    }\n\n    async loadChildrenArticles(parentId) {\n        return rpc(\"/knowledge/public_sidebar/children\", { parent_id: parentId });\n    }\n\n    /**\n     * Renders the tree listing all articles.\n     * To minimize loading time, the function will initially load the root\n     * articles.\n     * The other articles will be loaded lazily: The user will have to click on\n     * the caret next to an article to load and see their children.\n     * The id of the unfolded articles will be cached so that they will\n     * automatically be displayed on page load.\n     */\n    async renderTree() {\n        const params = new URLSearchParams(document.location.search);\n        if (params.get(\"auto_unfold\")) {\n            this.unfoldedArticlesIds.push(this.props.record.resId);\n        }\n        try {\n            this.state.tree = markup(\n                await this.keepLastRender.add(\n                    rpc(\"/knowledge/public_sidebar\", {\n                        active_article_id: this.props.record.resId,\n                        unfolded_articles_ids: this.unfoldedArticlesIds,\n                    })\n                )\n            );\n        } catch {\n            this.state.tree = undefined;\n        }\n    }\n\n    /**\n     * Enables the user to resize the aside block.\n     * Note: When the user grabs the resizer, a new listener will be attached\n     * to the document. The listener will be removed as soon as the user releases\n     * the resizer to free some resources.\n     */\n    resizeSidebar() {\n        const onPointerMove = throttleForAnimation((event) => {\n            event.preventDefault();\n            this.state.sidebarSize = event.pageX;\n        });\n        const onPointerUp = () => {\n            document.removeEventListener(\"pointermove\", onPointerMove);\n            document.body.style.cursor = \"auto\";\n            document.body.style.userSelect = \"auto\";\n        };\n        // Add style to root element because resizing has a transition delay,\n        // meaning that the cursor is not always on top of the resizer.\n        document.body.style.cursor = \"col-resize\";\n        document.body.style.userSelect = \"none\";\n        document.addEventListener(\"pointermove\", onPointerMove);\n        document.addEventListener(\"pointerup\", onPointerUp, { once: true });\n    }\n\n    async searchArticles(ev) {\n        ev.preventDefault();\n        const searchTerm = ev.target.value;\n        if (!searchTerm) {\n            await this.renderTree();\n            return;\n        }\n        try {\n            this.state.tree = markup(\n                await this.keepLastRender.add(\n                    rpc(\"/knowledge/public_sidebar\", {\n                        search_term: searchTerm,\n                        active_article_id: this.props.record.resId,\n                    })\n                )\n            );\n        } catch {\n            this.state.tree = undefined;\n        }\n    }\n\n    toggleAsideMobile() {\n        this.state.showAsideMobile = !this.state.showAsideMobile;\n    }\n}\n\nregistry.category(\"public_components\").add(\"knowledge.public_view\", KnowledgePublic);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { addLoadingEffect } from '@web/core/utils/ui';\nimport { browser } from \"@web/core/browser/browser\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { attachComponent } from \"@web_editor/js/core/owl_utils\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nclass WebsiteLinksTagsWrapper extends Component {\n    static template = \"website_links.WebsiteLinksTagsWrapper\";\n    static components = { SelectMenu, DropdownItem };\n    static props = {\n        placeholder: { optional: true, type: String },\n        model: { optional: true, type: String },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.keepLast = new KeepLast();\n        this.state = useState({\n            placeholder: this.props.placeholder,\n            choices: [],\n            value: undefined,\n        });\n        onWillStart(async () => {\n            this.canCreateLinkTracker = await this.orm.call(this.props.model, \"has_access\", [[], \"create\"]);\n            await this.loadChoice();\n        });\n    }\n\n    get showCreateOption() {\n        return this.select.data.searchValue && !this.state.choices.some(c => c.label === this.select.data.searchValue) && this.canCreateLinkTracker;\n    }\n\n    onSelect(value) {\n        this.state.value = value;\n    }\n\n    async onCreateOption(string, closeFn) {\n        const record = await this.orm.call(\"utm.mixin\", \"find_or_create_record\", [\n            this.props.model,\n            string,\n        ]);\n        const choice = {\n            label: record.name,\n            value: record.id,\n        };\n        this.state.choices.push(choice);\n        this.onSelect(choice.value);\n    }\n\n    loadChoice(searchString = \"\") {\n        return new Promise((resolve, reject) => {\n            // We want to search with a limit and not care about any\n            // pagination implementation. To make this work, we\n            // display the exact match first though, which requires\n            // an extra RPC (could be refactored into a new\n            // controller in master but... see TODO).\n            // TODO at some point this whole app will be moved as a\n            // backend screen, with real m2o fields etc... in which\n            // case the \"exact match\" feature should be handled by\n            // the ORM somehow ?\n            const limit = 100;\n            const searchReadParams = [\n                [\"id\", \"name\"],\n                {\n                    limit: limit,\n                    order: \"name, id desc\", // Allows to have exact match first\n                },\n            ];\n            const proms = [];\n            proms.push(\n                this.orm.searchRead(\n                    this.props.model,\n                    // Exact match + results that start with the search\n                    [[\"name\", \"=ilike\", `${searchString}%`]],\n                    ...searchReadParams\n                )\n            );\n            proms.push(\n                this.orm.searchRead(\n                    this.props.model,\n                    // Results that contain the search but do not start\n                    // with it\n                    [[\"name\", \"=ilike\", `%_${searchString}%`]],\n                    ...searchReadParams\n                )\n            );\n            // Keep last is there in case a RPC takes longer than\n            // the debounce delay + next rpc delay for some reason.\n            this.keepLast\n                .add(Promise.all(proms))\n                .then(([startingMatches, endingMatches]) => {\n                    const formatChoice = (choice) => {\n                        choice.value = choice.id;\n                        choice.label = choice.name;\n                        return choice;\n                    };\n                    startingMatches.map(formatChoice);\n\n                    // We loaded max a 2 * limit amount of records but\n                    // ensure that we do not display \"ending matches\" if\n                    // we may not have loaded all \"starting matches\".\n                    if (startingMatches.length < limit) {\n                        const startingMatchesId = startingMatches.map((value) => value.id);\n                        const extraEndingMatches = endingMatches.filter(\n                            (value) => !startingMatchesId.includes(value.id)\n                        );\n                        extraEndingMatches.map(formatChoice);\n                        return startingMatches.concat(extraEndingMatches);\n                    }\n                    // In that case, we made one RPC too much but this\n                    // was chosen over not making them go in parallel.\n                    // We don't want to display \"ending matches\" if not\n                    // all \"starting matches\" have been loaded.\n                    return startingMatches;\n                })\n                .then((result) => {\n                    this.state.choices = result;\n                    resolve();\n                })\n                .catch(reject);\n        });\n    }\n}\n\nvar RecentLinkBox = publicWidget.Widget.extend({\n    template: 'website_links.RecentLink',\n    events: {\n        'click .btn_shorten_url_clipboard': '_onCopyShortenUrl',\n    },\n\n    /**\n     * @constructor\n     * @param {Object} parent\n     * @param {Object} obj\n     */\n    init: function (parent, obj) {\n        this._super.apply(this, arguments);\n        this.link_obj = obj;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onCopyShortenUrl: async function (ev) {\n        ev.preventDefault();\n        const copyBtn = ev.currentTarget;\n        const tooltip = Tooltip.getOrCreateInstance(copyBtn, {\n            title: _t(\"Link Copied!\"),\n            trigger: \"manual\",\n            placement: \"top\",\n        });\n        setTimeout(\n            async () => await browser.navigator.clipboard.writeText(copyBtn.dataset.url)\n        );\n        tooltip.show();\n        setTimeout(() => tooltip.hide(), 1200);\n    },\n});\n\nvar RecentLinks = publicWidget.Widget.extend({\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    getRecentLinks: function (filter) {\n        var self = this;\n        return rpc('/website_links/recent_links', {\n            filter: filter,\n            limit: 20,\n        }).then(function (result) {\n            result.reverse().forEach((link) => {\n                self._addLink(link);\n            });\n            self._updateNotification();\n            self._updateFilters(filter);\n        }, function () {\n            var message = _t(\"Unable to get recent links\");\n            self.$el.append('<div class=\"alert alert-danger\">' + message + '</div>');\n        });\n    },\n    /**\n     * @private\n     */\n    _addLink: function (link) {\n        var nbLinks = this.getChildren().length;\n        var recentLinkBox = new RecentLinkBox(this, link);\n        recentLinkBox.prependTo(this.$el);\n        $('.link-tooltip').tooltip();\n\n        if (nbLinks === 0) {\n            this._updateNotification();\n        }\n    },\n    /**\n     * @private\n     */\n    removeLinks: function () {\n        this.getChildren().forEach((child) => {\n            child.destroy();\n        });\n    },\n    /**\n     * @private\n     * Updates the dropdown with the selected filter\n     */\n    _updateFilters: function(filter) {\n        const dropdownBtns = document.querySelectorAll('#recent_links_sort_by a');\n        dropdownBtns.forEach((button) => {\n            if (button.dataset.filter === filter) {\n                document.querySelector('.o_website_links_sort_by').textContent = button.textContent;\n                button.classList.add('active');\n            } else {\n                button.classList.remove('active');\n            }\n        });\n    },\n    /**\n     * @private\n     */\n    _updateNotification: function () {\n        if (this.getChildren().length === 0) {\n            var message = _t(\"You don't have any recent links.\");\n            $('.o_website_links_recent_links_notification').html('<div class=\"alert alert-info\">' + message + '</div>');\n        } else {\n            $('.o_website_links_recent_links_notification').empty();\n        }\n    },\n});\n\npublicWidget.registry.websiteLinks = publicWidget.Widget.extend({\n    selector: '.o_website_links_create_tracked_url',\n    events: {\n        'click #recent_links_sort_by a': '_onRecentLinksFilterChange',\n        'click .o_website_links_new_link_tracker': '_onCreateNewLinkTrackerClick',\n        'submit #o_website_links_link_tracker_form': '_onFormSubmit',\n    },\n\n    /**\n     * @override\n     */\n    start: async function () {\n        var defs = [this._super.apply(this, arguments)];\n\n        async function attachSelectComponent(model, placeholderText, el) {\n            const props = {\n                placeholder: placeholderText,\n                model: model,\n            };\n            await attachComponent(this, el, WebsiteLinksTagsWrapper, props);\n        }\n\n        attachSelectComponent.call(\n            this,\n            \"utm.campaign\",\n            _t(\"e.g. June Sale, Paris Roadshow, ...\"),\n            this.el.querySelector(\"#campaign-select-wrapper\"),\n        );\n        attachSelectComponent.call(\n            this,\n            \"utm.medium\",\n            _t(\"e.g. InMails, Ads, Social, ...\"),\n            this.el.querySelector(\"#channel-select-wrapper\"),\n        );\n        attachSelectComponent.call(\n            this,\n            \"utm.source\",\n            _t(\"e.g. LinkedIn, Facebook, Leads, ...\"),\n            this.el.querySelector(\"#source-select-wrapper\"),\n        );\n\n        // Recent Links Widgets\n        this.recentLinks = new RecentLinks(this);\n        defs.push(this.recentLinks.appendTo($('#o_website_links_recent_links')));\n        this.recentLinks.getRecentLinks('newest');\n\n        $('[data-bs-toggle=\"tooltip\"]').tooltip();\n\n        return Promise.all(defs);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onRecentLinksFilterChange(ev) {\n        this.recentLinks.removeLinks();\n        this.recentLinks.getRecentLinks(ev.currentTarget.dataset.filter);\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     * Show the link tracker form back\n     */\n    _onCreateNewLinkTrackerClick: function (ev) {\n        const utmForm = document.querySelector(\".o_website_links_utm_forms\");\n        if (!utmForm.classList.contains(\"d-none\")) {\n            return;\n        }\n        utmForm.classList.remove(\"d-none\");\n        document.querySelector(\"#generated_tracked_link\").classList.add(\"d-none\");\n        document.querySelector(\"#btn_shorten_url\").classList.remove(\"d-none\");\n        document.querySelector(\"input#url\").value = '';\n    },\n    /**\n     * Add the RecentLinkBox widget and send the form when the user generate the link\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onFormSubmit: function (ev) {\n        var self = this;\n        ev.preventDefault();\n        const generateLinkTrackerBtn = document.querySelector(\"#btn_shorten_url\");\n        if (generateLinkTrackerBtn.classList.contains(\"d-none\")) {\n            return;\n        }\n        const restoreLoadingBtn = addLoadingEffect(generateLinkTrackerBtn);\n\n        ev.stopPropagation();\n\n        // Get URL and UTMs\n        const campaignInputEl = document.querySelector(\"input[name='campaign-select']\");\n        const mediumInputEl = document.querySelector(\"input[name='medium-select']\");\n        const sourceInputEl = document.querySelector(\"input[name='source-select']\");\n\n        const label = document.querySelector('#label');\n        const params = { label: label.value || undefined };\n        params.url = $('#url').val();\n        if (campaignInputEl.value !== \"\") {\n            params.campaign_id = parseInt(campaignInputEl.value);\n        }\n        if (mediumInputEl.value !== \"\") {\n            params.medium_id = parseInt(mediumInputEl.value);\n        }\n        if (sourceInputEl.value !== \"\") {\n            params.source_id = parseInt(sourceInputEl.value);\n        }\n\n        rpc('/website_links/new', params).then(function (result) {\n            restoreLoadingBtn();\n            if ('error' in result) {\n                // Handle errors\n                if (result.error === 'empty_url') {\n                    $('.notification').html('<div class=\"alert alert-danger\">The URL is empty.</div>');\n                } else if (result.error === 'url_not_found') {\n                    $('.notification').html('<div class=\"alert alert-danger\">URL not found (404)</div>');\n                } else {\n                    $('.notification').html('<div class=\"alert alert-danger\">An error occur while trying to generate your link. Try again later.</div>');\n                }\n            } else {\n                // Link generated, clean the form and show the link\n                var link = result[0];\n\n                document.querySelector(\"#generated_tracked_link\").classList.remove(\"d-none\");\n                document.querySelector(\"#btn_shorten_url\").classList.add(\"d-none\");\n\n                document.querySelector(\".copy-to-clipboard\").dataset.clipboardText = link.short_url;\n                document.querySelector(\"#short-url-host\").textContent = link.short_url_host;\n                document.querySelector(\"#o_website_links_code\").textContent = link.code;\n\n                self.recentLinks._addLink(link);\n\n                // Clean notifications, URL and UTM selects\n                $('.notification').html('');\n                campaignInputEl.value = \"\";\n                mediumInputEl.value = \"\";\n                sourceInputEl.value = \"\";\n                label.value = '';\n                document.querySelector(\".o_website_links_utm_forms\").classList.add(\"d-none\");\n            }\n        });\n    },\n});\n\nexport default {\n    RecentLinkBox: RecentLinkBox,\n    RecentLinks: RecentLinks,\n};\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { rpc } from \"@web/core/network/rpc\";\n\npublicWidget.registry.websiteLinksCodeEditor = publicWidget.Widget.extend({\n    selector: '#wrapwrap',\n    selectorHas: '.o_website_links_edit_code',\n    events: {\n        'click .copy-to-clipboard': '_onCopyToClipboardClick',\n        'click .o_website_links_edit_code': '_onEditCodeClick',\n        'click .o_website_links_cancel_edit': '_onCancelEditClick',\n        'submit #edit-code-form': '_onEditCodeFormSubmit',\n        'click .o_website_links_ok_edit': '_onEditCodeFormSubmit',\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onCopyToClipboardClick: async function (ev) {\n        ev.preventDefault();\n        const copyBtn = ev.currentTarget;\n        const tooltip = Tooltip.getOrCreateInstance(copyBtn, {\n            title: _t(\"Link Copied!\"),\n            trigger: \"manual\",\n            placement: \"right\",\n        });\n        setTimeout(\n            async () => await browser.navigator.clipboard.writeText(copyBtn.dataset.clipboardText)\n        );\n        tooltip.show();\n        setTimeout(() => tooltip.hide(), 1200);\n    },\n\n    /**\n     * @private\n     * @param {String} newCode\n     */\n    _showNewCode: function (newCode) {\n        $('.o_website_links_code_error').html('');\n        $('.o_website_links_code_error').hide();\n\n        $('#o_website_links_code form').remove();\n\n        // Show new code\n        var host = $('#short-url-host').html();\n        $('#o_website_links_code').html(newCode);\n\n        // Update button copy to clipboard\n        $('.copy-to-clipboard').attr('data-clipboard-text', host + newCode);\n\n        // Show action again\n        $('.o_website_links_edit_code').show();\n        $('.copy-to-clipboard').show();\n        $('.o_website_links_edit_tools').hide();\n    },\n    /**\n     * @private\n     * @returns {Promise}\n     */\n    _submitCode: function () {\n        var initCode = $('#edit-code-form #init_code').val();\n        var newCode = $('#edit-code-form #new_code').val();\n        var self = this;\n\n        if (newCode === '') {\n            self.$('.o_website_links_code_error').html(_t(\"The code cannot be left empty\"));\n            self.$('.o_website_links_code_error').show();\n            return;\n        }\n\n        this._showNewCode(newCode);\n\n        if (initCode === newCode) {\n            this._showNewCode(newCode);\n        } else {\n            return rpc('/website_links/add_code', {\n                init_code: initCode,\n                new_code: newCode,\n            }).then(function (result) {\n                self._showNewCode(result[0].code);\n            }, function () {\n                $('.o_website_links_code_error').show();\n                $('.o_website_links_code_error').html(_t(\"This code is already taken\"));\n            });\n        }\n\n        return Promise.resolve();\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onEditCodeClick: function () {\n        var initCode = $('#o_website_links_code').html();\n        $('#o_website_links_code').html('<form style=\"display:inline;\" id=\"edit-code-form\"><input type=\"hidden\" id=\"init_code\" value=\"' + initCode + '\"/><input type=\"text\" id=\"new_code\" value=\"' + initCode + '\"/></form>');\n        $('.o_website_links_edit_code').hide();\n        $('.copy-to-clipboard').hide();\n        $('.o_website_links_edit_tools').show();\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onCancelEditClick: function (ev) {\n        ev.preventDefault();\n        $('.o_website_links_edit_code').show();\n        $('.copy-to-clipboard').show();\n        $('.o_website_links_edit_tools').hide();\n        $('.o_website_links_code_error').hide();\n\n        var oldCode = $('#edit-code-form #init_code').val();\n        $('#o_website_links_code').html(oldCode);\n\n        $('#code-error').remove();\n        $('#o_website_links_code form').remove();\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onEditCodeFormSubmit: function (ev) {\n        ev.preventDefault();\n        this._submitCode();\n    },\n});\n", "/** @odoo-module **/\n\nimport { loadBundle } from \"@web/core/assets\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nconst { DateTime } = luxon;\n\nvar BarChart = publicWidget.Widget.extend({\n    /**\n     * @constructor\n     * @param {Object} parent\n     * @param {Object} beginDate\n     * @param {Object} endDate\n     * @param {Object} dates\n     */\n    init: function (parent, beginDate, endDate, dates) {\n        this._super.apply(this, arguments);\n        this.beginDate = beginDate.startOf(\"day\");\n        this.endDate = endDate.startOf(\"day\");\n        if (this.beginDate.toISO() === this.endDate.toISO()) {\n            this.endDate = this.endDate.plus({ days: 1 });\n        }\n        this.number_of_days = this.endDate.diff(this.beginDate).as(\"days\");\n        this.dates = dates;\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        // Fill data for each day (with 0 click for days without data)\n        var clicksArray = [];\n        for (var i = 0; i <= this.number_of_days; i++) {\n            var dateKey = this.beginDate.toFormat(\"yyyy-MM-dd\");\n            clicksArray.push([dateKey, (dateKey in this.dates) ? this.dates[dateKey] : 0]);\n            this.beginDate = this.beginDate.plus({ days: 1 });\n        }\n\n        var nbClicks = 0;\n        var data = [];\n        var labels = [];\n        clicksArray.forEach(function (pt) {\n            labels.push(pt[0]);\n            nbClicks += pt[1];\n            data.push(pt[1]);\n        });\n\n        this.$('.title').text(_t('%(clicks)s clicks', {clicks: nbClicks}));\n\n        var config = {\n            type: 'line',\n            data: {\n                labels: labels,\n                datasets: [{\n                    data: data,\n                    fill: 'start',\n                    label: _t('# of clicks'),\n                    backgroundColor: '#ebf2f7',\n                    borderColor: '#6aa1ca',\n\n                }],\n            },\n            options: {\n                scales: {\n                    y: {\n                        ticks: {\n                            callback: function(value) {\n                                if (Number.isInteger(value)) {\n                                    return value;\n                                }\n                            },\n                        }\n                    }\n                }\n            }\n        };\n        var canvas = this.$('canvas')[0];\n        var context = canvas.getContext('2d');\n        new Chart(context, config);\n    },\n    willStart: async function () {\n        await loadBundle(\"web.chartjs_lib\");\n    },\n});\n\nvar PieChart = publicWidget.Widget.extend({\n    /**\n     * @override\n     * @param {Object} parent\n     * @param {Object} data\n     */\n    init: function (parent, data) {\n        this._super.apply(this, arguments);\n        this.data = data;\n    },\n    /**\n     * @override\n     */\n    start: function () {\n\n        // Process country data to fit into the ChartJS scheme\n        var labels = [];\n        var data = [];\n        for (var i = 0; i < this.data.length; i++) {\n            var countryName = this.data[i]['country_id'] ? this.data[i]['country_id'][1] : _t('Undefined');\n            labels.push(countryName + ' (' + this.data[i]['country_id_count'] + ')');\n            data.push(this.data[i]['country_id_count']);\n        }\n\n        // Set title\n        this.$('.title').text(_t('%(count)s countries', {count: this.data.length}));\n\n        var config = {\n            type: 'pie',\n            data: {\n                labels: labels,\n                datasets: [{\n                    data: data,\n                    label: this.data.length > 0 ? this.data[0].key : _t('No data'),\n                }]\n            },\n            options: {\n                aspectRatio: 2,\n            },\n        };\n\n        var canvas = this.$('canvas')[0];\n        var context = canvas.getContext('2d');\n        new Chart(context, config);\n    },\n    willStart: async function () {\n        await loadBundle(\"web.chartjs_lib\");\n    },\n});\n\npublicWidget.registry.websiteLinksCharts = publicWidget.Widget.extend({\n    selector: '.o_website_links_chart',\n\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n    },\n\n    /**\n     * @override\n     */\n    start: async function () {\n        var self = this;\n        this.charts = {};\n\n        // Get the code of the link\n        var linkID = parseInt($('#link_id').val());\n        this.links_domain = ['link_id', '=', linkID];\n\n        var defs = [];\n        defs.push(this._totalClicks());\n        defs.push(this._clicksByDay());\n        defs.push(this._clicksByCountry());\n        defs.push(this._lastWeekClicksByCountry());\n        defs.push(this._lastMonthClicksByCountry());\n        defs.push(this._super.apply(this, arguments));\n\n        this.animating_copy = false;\n\n        return Promise.all(defs).then(function (results) {\n            var _totalClicks = results[0];\n            var _clicksByDay = results[1];\n            var _clicksByCountry = results[2];\n            var _lastWeekClicksByCountry = results[3];\n            var _lastMonthClicksByCountry = results[4];\n\n            if (!_totalClicks) {\n                $('#all_time_charts').prepend(_t(\"There is no data to show\"));\n                $('#last_month_charts').prepend(_t(\"There is no data to show\"));\n                $('#last_week_charts').prepend(_t(\"There is no data to show\"));\n                return;\n            }\n\n            var formattedClicksByDay = {};\n            var beginDate;\n            for (var i = 0; i < _clicksByDay.length; i++) {\n                // This is a trick to get the date without the local formatting.\n                // We can't simply do .locale(\"en\") because some Odoo languages\n                // are not supported by moment.js (eg: Arabic Syria).\n                // FIXME this now uses luxon, check if this is still needed? Probably can be replaced by deserializeDate\n                const date = DateTime.fromFormat(\n                    _clicksByDay[i][\"__domain\"].find((el) => el.length && el.includes(\">=\"))[2]\n                        .split(\" \")[0], \"yyyy-MM-dd\"\n                );\n                if (i === 0) {\n                    beginDate = date;\n                }\n                formattedClicksByDay[date.setLocale(\"en\").toFormat(\"yyyy-MM-dd\")] =\n                    _clicksByDay[i][\"create_date_count\"];\n            }\n\n            // Process all time line chart data\n            var now = DateTime.now();\n            self.charts.all_time_bar = new BarChart(self, beginDate, now, formattedClicksByDay);\n            self.charts.all_time_bar.attachTo($('#all_time_clicks_chart'));\n\n            // Process month line chart data\n            beginDate = DateTime.now().minus({ days: 30 });\n            self.charts.last_month_bar = new BarChart(self, beginDate, now, formattedClicksByDay);\n            self.charts.last_month_bar.attachTo($('#last_month_clicks_chart'));\n\n            // Process week line chart data\n            beginDate = DateTime.now().minus({ days: 7 });\n            self.charts.last_week_bar = new BarChart(self, beginDate, now, formattedClicksByDay);\n            self.charts.last_week_bar.attachTo($('#last_week_clicks_chart'));\n\n            // Process pie charts\n            self.charts.all_time_pie = new PieChart(self, _clicksByCountry);\n            self.charts.all_time_pie.attachTo($('#all_time_countries_charts'));\n\n            self.charts.last_month_pie = new PieChart(self, _lastMonthClicksByCountry);\n            self.charts.last_month_pie.attachTo($('#last_month_countries_charts'));\n\n            self.charts.last_week_pie = new PieChart(self, _lastWeekClicksByCountry);\n            self.charts.last_week_pie.attachTo($('#last_week_countries_charts'));\n\n            var rowWidth = $('#all_time_countries_charts').parent().width();\n            var $chartCanvas = $('#all_time_countries_charts,last_month_countries_charts,last_week_countries_charts').find('canvas');\n            $chartCanvas.height(Math.max(_clicksByCountry.length * (rowWidth > 750 ? 1 : 2), 20) + 'em');\n\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _totalClicks: function () {\n        return this.orm.searchCount(\"link.tracker.click\", [this.links_domain]);\n    },\n    /**\n     * @private\n     */\n    _clicksByDay: function () {\n        return this.orm.readGroup(\n            \"link.tracker.click\",\n            [this.links_domain],\n            [\"create_date\"],\n            [\"create_date:day\"]\n        );\n    },\n    /**\n     * @private\n     */\n    _clicksByCountry: function () {\n        return this.orm.readGroup(\n            \"link.tracker.click\",\n            [this.links_domain],\n            [\"country_id\"],\n            [\"country_id\"]\n        );\n    },\n    /**\n     * @private\n     */\n    _lastWeekClicksByCountry: function () {\n        // 7 days * 24 hours * 60 minutes * 60 seconds * 1000 milliseconds.\n        const aWeekAgoDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\n        // get the date in the format YYYY-MM-DD.\n        const aWeekAgoString = aWeekAgoDate.toISOString().split(\"T\")[0];\n        return this.orm.readGroup(\n            \"link.tracker.click\",\n            [this.links_domain, [\"create_date\", \">\", aWeekAgoString]],\n            [\"country_id\"],\n            [\"country_id\"]\n        );\n    },\n    /**\n     * @private\n     */\n    _lastMonthClicksByCountry: function () {\n        // 30 days * 24 hours * 60 minutes * 60 seconds * 1000 milliseconds.\n        const aMonthAgoDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);\n        // get the date in the format YYYY-MM-DD.\n        const aMonthAgoString = aMonthAgoDate.toISOString().split(\"T\")[0];\n        return this.orm.readGroup(\n            \"link.tracker.click\",\n            [this.links_domain, [\"create_date\", \">\", aMonthAgoString]],\n            [\"country_id\"],\n            [\"country_id\"]\n        );\n    },\n});\n\nexport default {\n    BarChart: BarChart,\n    PieChart: PieChart,\n};\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport {ReCaptcha} from \"@google_recaptcha/js/recaptcha\";\nimport { rpc } from \"@web/core/network/rpc\";\n\npublicWidget.registry.subscribe = publicWidget.Widget.extend({\n    selector: \".js_subscribe\",\n    disabledInEditableMode: false,\n    read_events: {\n        'click .js_subscribe_btn': '_onSubscribeClick',\n    },\n\n    /**\n     * @constructor\n     */\n    init: function () {\n        this._super(...arguments);\n        this._recaptcha = new ReCaptcha();\n        this.notification = this.bindService(\"notification\");\n    },\n    /**\n     * @override\n     */\n    willStart: function () {\n        this._recaptcha.loadLibs();\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        var def = this._super.apply(this, arguments);\n\n        if (this.editableMode) {\n            // Since there is an editor option to choose whether \"Thanks\" button\n            // should be visible or not, we should not vary its visibility here.\n            return def;\n        }\n        const always = this._updateView.bind(this);\n        const inputName = this.el.querySelector('input').name;\n        return Promise.all([def, rpc('/website_mass_mailing/is_subscriber', {\n            'list_id': this._getListId(),\n            'subscription_type': inputName,\n        }).then(always, always)]);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._updateView({is_subscriber: false});\n        this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Modifies the elements to have the view of a subscriber/non-subscriber.\n     *\n     * @todo should probably be merged with _updateSubscribeControlsStatus\n     * @param {Object} data\n     */\n    _updateView(data) {\n        this._updateSubscribeControlsStatus(!!data.is_subscriber);\n\n        // js_subscribe_email is kept by compatibility (it was the old name of js_subscribe_value)\n        const valueInputEl = this.el.querySelector('input.js_subscribe_value, input.js_subscribe_email');\n        valueInputEl.value = data.value || '';\n\n        // Compat: remove d-none for DBs that have the button saved with it.\n        this.el.classList.remove('d-none');\n    },\n    /**\n     * Updates the visibility of the subscribe and subscribed buttons.\n     *\n     * @param {boolean} isSubscriber\n     */\n    _updateSubscribeControlsStatus(isSubscriber) {\n        const thanksWrapEl = this.el.querySelector('.js_subscribed_wrap');\n        const subscribeWrapEl = this.el.querySelector('.js_subscribe_wrap');\n        const subscribeBtnEl = this.el.querySelector('.js_subscribe_btn');\n\n        subscribeBtnEl.disabled = isSubscriber;\n        subscribeWrapEl.classList.toggle('d-none', isSubscriber);\n        thanksWrapEl.classList.toggle('d-none', !isSubscriber);\n\n        // js_subscribe_email is kept by compatibility (it was the old name of js_subscribe_value)\n        const valueInputEl = this.el.querySelector('input.js_subscribe_value, input.js_subscribe_email');\n        valueInputEl.disabled = isSubscriber;\n    },\n\n    _getListId: function () {\n        return this.$el.closest('[data-snippet=s_newsletter_block').data('list-id') || this.$el.data('list-id');\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onSubscribeClick: async function () {\n        var self = this;\n        const inputName = this.$('input').attr('name');\n        const $input = this.$(\".js_subscribe_value:visible, .js_subscribe_email:visible\"); // js_subscribe_email is kept by compatibility (it was the old name of js_subscribe_value)\n        if (inputName === 'email' && $input.length && !$input.val().match(/.+@.+/)) {\n            this.$el.addClass('o_has_error').find('.form-control').addClass('is-invalid');\n            return false;\n        }\n        this.$el.removeClass('o_has_error').find('.form-control').removeClass('is-invalid');\n        const tokenObj = await this._recaptcha.getToken('website_mass_mailing_subscribe');\n        if (tokenObj.error) {\n            self.notification.add(tokenObj.error, {\n                type: 'danger',\n                title: _t(\"Error\"),\n                sticky: true,\n            });\n            return false;\n        }\n        rpc('/website_mass_mailing/subscribe', {\n            'list_id': this._getListId(),\n            'value': $input.length ? $input.val() : false,\n            'subscription_type': inputName,\n            recaptcha_token_response: tokenObj.token,\n        }).then(function (result) {\n            let toastType = result.toast_type;\n            if (toastType === 'success') {\n                self._updateSubscribeControlsStatus(true);\n\n                const $popup = self.$el.closest('.o_newsletter_modal');\n                if ($popup.length) {\n                    $popup.modal('hide');\n                }\n            }\n            self.notification.add(result.toast_content, {\n                type: toastType,\n                title: toastType === 'success' ? _t('Success') : _t('Error'),\n                sticky: true,\n            });\n        });\n    },\n});\n\n/**\n * This widget tries to fix snippets that were malformed because of a missing\n * upgrade script. Without this, some newsletter snippets coming from users\n * upgraded from a version lower than 16.0 may not be able to update their\n * newsletter block.\n *\n * TODO an upgrade script should be made to fix databases and get rid of this.\n */\npublicWidget.registry.fixNewsletterListClass = publicWidget.Widget.extend({\n    selector: '.s_newsletter_subscribe_form:not(.s_subscription_list), .s_newsletter_block',\n\n    /**\n     * @override\n     */\n    start() {\n        this.$target[0].classList.add('s_newsletter_list');\n        return this._super(...arguments);\n    },\n});\n", "import { patch } from '@web/core/utils/patch';\nimport {\n    ComboConfiguratorDialog\n} from '@sale/js/combo_configurator_dialog/combo_configurator_dialog';\n\npatch(ComboConfiguratorDialog.prototype, {\n    async selectComboItem(comboId, comboItem) {\n        if (!comboItem.product.isQuantityAllowed(this.state.quantity)) {\n            return;\n        }\n        super.selectComboItem(...arguments);\n    },\n\n    /**\n     * Check whether the provided combo quantity can be added to the cart.\n     *\n     * @param {Number} quantity The quantity to check.\n     * @return {Boolean} Whether the combo quantity can be added to the cart.\n     */\n    isComboQuantityAllowed(quantity) {\n        return this._selectedComboItems.every(\n            comboItem => comboItem.product.isQuantityAllowed(quantity)\n        );\n    },\n});\n", "import { patch } from '@web/core/utils/patch';\nimport { ProductProduct } from '@sale/js/models/product_product';\n\npatch(ProductProduct.prototype, {\n    /**\n     * @param {number} free_qty\n     * @param args Super's parameter list.\n     */\n    setup({free_qty, ...args}) {\n        super.setup(args);\n        this.free_qty = free_qty;\n    },\n\n    /**\n     * Check whether the provided quantity can be added to the cart.\n     *\n     * @param {Number} quantity The quantity to check.\n     * @return {Boolean} Whether the product quantity can be added to the cart.\n     */\n    isQuantityAllowed(quantity) {\n        return this.free_qty === undefined || this.free_qty >= quantity;\n    },\n});\n", "/** @odoo-module **/\n\nimport { patch } from '@web/core/utils/patch';\nimport { Product } from '@sale/js/product/product';\n\npatch(Product, {\n    props: {\n        ...Product.props,\n        free_qty: { type: Number, optional: true },\n    },\n});\n\npatch(Product.prototype, {\n    /**\n     * Check whether this product is out of stock.\n     *\n     * @return {Boolean} - Whether this product is out of stock.\n     */\n    isOutOfStock() {\n        return !this.env.isQuantityAllowed(this.props, 1);\n    },\n});\n", "import { _t } from '@web/core/l10n/translation';\nimport { patch } from '@web/core/utils/patch';\nimport { ProductCard } from '@sale/js/product_card/product_card';\n\npatch(ProductCard, {\n    props: {\n        ...ProductCard.props,\n        quantity: { type: Number, optional: true },\n    },\n});\n\npatch(ProductCard.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.allQuantitySelectedTooltip = _t(\"All available quantity selected\");\n    },\n});\n", "/** @odoo-module **/\n\nimport { patch } from '@web/core/utils/patch';\nimport { useSubEnv } from '@odoo/owl';\nimport {\n    ProductConfiguratorDialog\n} from '@sale/js/product_configurator_dialog/product_configurator_dialog';\n\npatch(ProductConfiguratorDialog.prototype, {\n    setup() {\n        super.setup(...arguments);\n\n        useSubEnv({\n            isQuantityAllowed: this._isQuantityAllowed.bind(this),\n        });\n    },\n\n    async _setQuantity(productTmplId, quantity) {\n        const product = this._findProduct(productTmplId);\n        if (!this._isQuantityAllowed(product, quantity)) {\n            quantity = product.free_qty;\n        }\n        return super._setQuantity(productTmplId, quantity);\n    },\n\n    /**\n     * Check whether the provided product quantity can be added to the cart.\n     *\n     * @param {Object} product - The provided product.\n     * @param {Number} quantity - The new quantity of the product.\n     * @return {Boolean} - Whether the provided product quantity can be added to the cart.\n     */\n    _isQuantityAllowed(product, quantity) {\n        return product.free_qty === undefined || product.free_qty >= quantity;\n    },\n\n    /**\n     * Check whether all selected product quantities can be added to the cart.\n     *\n     * @return {Boolean} - Whether all selected product quantities can be added to the cart.\n     */\n    areQuantitiesAllowed() {\n        return this.state.products.every(p => this._isQuantityAllowed(p, p.quantity));\n    },\n});\n", "/** @odoo-module **/\n\nimport VariantMixin from \"@website_sale/js/sale_variant_mixin\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { renderToFragment } from \"@web/core/utils/render\";\nimport { formatFloat } from \"@web/core/utils/numbers\";\n\nimport \"@website_sale/js/website_sale\";\n\nimport { markup } from \"@odoo/owl\";\n\n/**\n * Addition to the variant_mixin._onChangeCombination\n *\n * This will prevent the user from selecting a quantity that is not available in the\n * stock for that product.\n *\n * It will also display various info/warning messages regarding the select product's stock.\n *\n * This behavior is only applied for the web shop (and not on the SO form)\n * and only for the main product.\n *\n * @param {MouseEvent} ev\n * @param {$.Element} $parent\n * @param {Array} combination\n */\nVariantMixin._onChangeCombinationStock = function (ev, $parent, combination) {\n    let product_id = 0;\n    // needed for list view of variants\n    if ($parent.find('input.product_id:checked').length) {\n        product_id = $parent.find('input.product_id:checked').val();\n    } else {\n        product_id = $parent.find('.product_id').val();\n    }\n    const isMainProduct = combination.product_id &&\n        $parent.is('.js_main_product') &&\n        combination.product_id === parseInt(product_id);\n\n    if (!this.isWebsite || !isMainProduct) {\n        return;\n    }\n\n    const $addQtyInput = $parent.find('input[name=\"add_qty\"]');\n    let qty = $addQtyInput.val();\n    let ctaWrapper = $parent[0].querySelector('#o_wsale_cta_wrapper');\n    ctaWrapper.classList.replace('d-none', 'd-flex');\n    ctaWrapper.classList.remove('out_of_stock');\n\n    if (combination.is_storable && !combination.allow_out_of_stock_order) {\n        combination.free_qty -= parseInt(combination.cart_qty);\n        $addQtyInput.data('max', combination.free_qty || 1);\n        if (combination.free_qty < 0) {\n            combination.free_qty = 0;\n        }\n        if (qty > combination.free_qty) {\n            qty = combination.free_qty || 1;\n            $addQtyInput.val(qty);\n        }\n        if (combination.free_qty < 1) {\n            ctaWrapper.classList.replace('d-flex', 'd-none');\n            ctaWrapper.classList.add('out_of_stock');\n        }\n    }\n\n    // needed xml-side for formatting of remaining qty\n    combination.formatQuantity = (qty) => {\n        if (Number.isInteger(qty)) {\n            return qty;\n        } else {\n            const decimals = Math.max(\n                0,\n                Math.ceil(-Math.log10(combination.uom_rounding))\n            );\n            return formatFloat(qty, {digits: [false, decimals]});\n        }\n    }\n\n    $('.oe_website_sale')\n        .find('.availability_message_' + combination.product_template)\n        .remove();\n    combination.has_out_of_stock_message = $(combination.out_of_stock_message).text() !== '';\n    combination.out_of_stock_message = markup(combination.out_of_stock_message);\n    $('div.availability_messages').append(renderToFragment(\n        'website_sale_stock.product_availability',\n        combination\n    ));\n};\n\npublicWidget.registry.WebsiteSale.include({\n    /**\n     * Adds the stock checking to the regular _onChangeCombination method\n     * @override\n     */\n    _onChangeCombination: function () {\n        this._super.apply(this, arguments);\n        VariantMixin._onChangeCombinationStock.apply(this, arguments);\n    },\n    /**\n     * Recomputes the combination after adding a product to the cart\n     * @override\n     */\n    _onClickAdd(ev) {\n        return this._super.apply(this, arguments).then(() => {\n            if ($('div.availability_messages').length) {\n                this._getCombinationInfo(ev);\n            }\n        });\n    }\n});\n\nexport default VariantMixin;\n", "/** @odoo-module **/\n\nimport { WebsiteSale } from '@website_sale/js/website_sale';\nimport { rpc } from \"@web/core/network/rpc\";\nimport { isEmail } from '@web/core/utils/strings';\n\nWebsiteSale.include({\n    events: Object.assign({}, WebsiteSale.prototype.events, {\n        'click #product_stock_notification_message': '_onClickProductStockNotificationMessage',\n        'click #product_stock_notification_form_submit_button': '_onClickSubmitProductStockNotificationForm',\n    }),\n\n    _onClickProductStockNotificationMessage: function (ev) {\n        const partnerEmail = document.querySelector('#wsale_user_email').value;\n        const emailInputEl = document.querySelector('#stock_notification_input');\n\n        emailInputEl.value = partnerEmail;\n        this._handleClickStockNotificationMessage(ev);\n    },\n\n    _onClickSubmitProductStockNotificationForm: function (ev) {\n        const formEl = ev.currentTarget.closest('#stock_notification_form');\n        const productId = parseInt(formEl.querySelector('input[name=\"product_id\"]').value);\n        this._handleClickSubmitStockNotificationForm(ev, productId);\n    },\n\n\n    _handleClickStockNotificationMessage(ev) {\n        ev.currentTarget.classList.add('d-none');\n        ev.currentTarget.parentElement.querySelector('#stock_notification_form').classList.remove('d-none');\n    },\n\n    _handleClickSubmitStockNotificationForm(ev, productId) {\n        const stockNotificationEl = ev.currentTarget.closest('#stock_notification_div');\n        const formEl = stockNotificationEl.querySelector('#stock_notification_form');\n        const email = stockNotificationEl.querySelector('#stock_notification_input').value.trim();\n\n        if (!isEmail(email)) {\n            return this._displayEmailIncorrectMessage(stockNotificationEl);\n        }\n\n        rpc(\"/shop/add/stock_notification\", {\n            product_id: productId,\n            email,\n        }).then((data) => {\n            const message = stockNotificationEl.querySelector('#stock_notification_success_message');\n\n            message.classList.remove('d-none');\n            formEl.classList.add('d-none');\n        }).catch((error) => {\n            this._displayEmailIncorrectMessage(stockNotificationEl);\n        });\n    },\n\n    _displayEmailIncorrectMessage(stockNotificationEl) {\n        const incorrectIconEl = stockNotificationEl.querySelector('#stock_notification_input_incorrect');\n        incorrectIconEl.classList.remove('d-none');\n    }\n});\n\nexport default WebsiteSale;\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ReorderDialog } from \"@website_sale/js/website_sale_reorder\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ReorderDialog.prototype, {\n    /**\n     * @override\n     */\n    async onWillStartHandler() {\n        const res = await super.onWillStartHandler(...arguments);\n        for (const product of this.content.products) {\n            this.stockCheckCombinationInfo(product);\n        }\n        return res;\n    },\n\n    /**\n     * @override\n     */\n    async loadProductCombinationInfo(product) {\n        await super.loadProductCombinationInfo(...arguments);\n    },\n\n    stockCheckCombinationInfo(product) {\n        // Products that should have a max quantity available should be limited by default.\n        if (product.combinationInfo.allow_out_of_stock_order || ! product.is_storable) {\n            return;\n        }\n        product.max_quantity_available = product.combinationInfo.free_qty;\n        if (!product.max_quantity_available) {\n            product.add_to_cart_allowed = false;\n        }\n        if (product.max_quantity_available < product.qty) {\n            product.qty_warning = _t(\n                \"You ask for %(quantity1)s Units but only %(quantity2)s are available.\",\n                {\n                    quantity1: product.qty.toFixed(1),\n                    quantity2: product.max_quantity_available.toFixed(1),\n                }\n            );\n            product.qty = product.max_quantity_available;\n            product.stock_warning = true;\n        } else if (product.combinationInfo.cart_qty) {\n            product.qty_warning = _t(\n                \"You already have %s Units in your cart.\",\n                product.combinationInfo.cart_qty.toFixed(1)\n            );\n        }\n    },\n\n    /**\n     * @override\n     */\n    getWarningForProduct(product) {\n        if (product.hasOwnProperty(\"max_quantity_available\") && !product.max_quantity_available) {\n            return _t(\"This product is out of stock.\");\n        }\n        return super.getWarningForProduct(...arguments);\n    },\n\n    /**\n     * @override\n     */\n    changeProductQty(product, newQty) {\n        if (product.max_quantity_available && newQty > product.max_quantity_available) {\n            product.qty_warning = _t(\n                \"You ask for %(quantity1)s Units but only %(quantity2)s are available.\",\n                {\n                    quantity1: newQty.toFixed(1),\n                    quantity2: product.max_quantity_available.toFixed(1),\n                }\n            );\n            product.stock_warning = true;\n            newQty = product.max_quantity_available;\n        } else if (product.stock_warning) {\n            product.qty_warning = false;\n            product.stock_warning = false;\n        }\n        super.changeProductQty(product, newQty);\n    },\n});\n", "/** @odoo-module **/\n\nimport { WebsiteSale } from '@website_sale/js/website_sale';\n\nWebsiteSale.include({\n    /**\n     * Toggles the add to cart button depending on the possibility of the\n     * current combination.\n     *\n     * @override\n     */\n    _toggleDisable: function ($parent, isCombinationPossible) {\n        this._super(...arguments);\n        $parent.find('button.o_wish_add').toggleClass('disabled', !isCombinationPossible);\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport wSaleUtils from \"@website_sale/js/website_sale_utils\";\nimport VariantMixin from \"@website_sale/js/sale_variant_mixin\";\nimport { rpc, RPCError } from \"@web/core/network/rpc\";\n\n// VariantMixin events are overridden on purpose here\n// to avoid registering them more than once since they are already registered\n// in website_sale.js\npublicWidget.registry.ProductWishlist = publicWidget.Widget.extend(VariantMixin, {\n    selector: '.oe_website_sale',\n    events: {\n        'click .o_wsale_my_wish': '_onClickMyWish',\n        'click .o_add_wishlist, .o_add_wishlist_dyn': '_onClickAddWish',\n        'change input.product_id': '_onChangeVariant',\n        'change input.js_product_change': '_onChangeProduct',\n        'click .wishlist-section .o_wish_rm': '_onClickWishRemove',\n        'click .wishlist-section .o_wish_add': '_onClickWishAdd',\n    },\n\n    /**\n     * @constructor\n     */\n    init: function (parent) {\n        this._super.apply(this, arguments);\n        this.wishlistProductIDs = JSON.parse(sessionStorage.getItem('website_sale_wishlist_product_ids') || '[]');\n    },\n    /**\n     * Gets the current wishlist items.\n     * In editable mode, do nothing instead.\n     *\n     * @override\n     */\n    willStart: function () {\n        var self = this;\n        var def = this._super.apply(this, arguments);\n        var wishDef;\n        if (this.wishlistProductIDs.length != +$('header#top .my_wish_quantity').text()) {\n            wishDef = $.get('/shop/wishlist', {\n                count: 1,\n            }).then(function (res) {\n                self.wishlistProductIDs = JSON.parse(res);\n                sessionStorage.setItem('website_sale_wishlist_product_ids', res);\n            });\n\n        }\n        return Promise.all([def, wishDef]);\n    },\n    /**\n     * Updates the wishlist view (navbar) & the wishlist button (product page).\n     * In editable mode, do nothing instead.\n     *\n     * @override\n     */\n    start: function () {\n        var def = this._super.apply(this, arguments);\n\n        this._updateWishlistView();\n        // trigger change on only one input\n        if (this.$('input.js_product_change').length) { // manage \"List View of variants\"\n            this.$('input.js_product_change:checked').first().trigger('change');\n        } else {\n            this.$('input.product_id').first().trigger('change');\n        }\n\n        return def;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _addNewProducts: function ($el) {\n        var self = this;\n        var productID = $el.data('product-product-id');\n        if ($el.hasClass('o_add_wishlist_dyn')) {\n            productID = parseInt($el.closest('.js_product').find('.product_id:checked').val());;\n        }\n        var $form = $el.closest('form');\n        var templateId = $form.find('.product_template_id').val();\n        // when adding from /shop instead of the product page, need another selector\n        if (!templateId) {\n            templateId = $el.data('product-template-id');\n        }\n        $el.prop(\"disabled\", true).addClass('disabled');\n        var productReady = this.selectOrCreateProduct(\n            $el.closest('form'),\n            productID,\n            templateId,\n        );\n\n        productReady.then(function (productId) {\n            productId = parseInt(productId, 10);\n\n            if (productId && !self.wishlistProductIDs.includes(productId)) {\n                return rpc('/shop/wishlist/add', {\n                    product_id: productId,\n                }).then(function () {\n                    var $navButton = $('header .o_wsale_my_wish').first();\n                    self.wishlistProductIDs.push(productId);\n                    sessionStorage.setItem('website_sale_wishlist_product_ids', JSON.stringify(self.wishlistProductIDs));\n                    self._updateWishlistView();\n                    wSaleUtils.animateClone($navButton, $el.closest('form'), 25, 40);\n                    // It might happen that `onChangeVariant` is called at the same time as this function.\n                    // In this case we need to set the button to disabled again.\n                    // Do this only if the productID is still the same.\n                    let currentProductId = $el.data('product-product-id');\n                    if ($el.hasClass('o_add_wishlist_dyn')) {\n                        currentProductId = parseInt($el.closest('.js_product').find('.product_id:checked').val());\n                    }\n                    if (productId === currentProductId) {\n                        $el.prop(\"disabled\", true).addClass('disabled');\n                    }\n                }).catch(function (e) {\n                    $el.prop(\"disabled\", false).removeClass('disabled');\n                    if (!(e instanceof RPCError)) {\n                        return Promise.reject(e);\n                    }\n                });\n            }\n        }).catch(function (e) {\n            $el.prop(\"disabled\", false).removeClass('disabled');\n            if (!(e instanceof RPCError)) {\n                return Promise.reject(e);\n            }\n        });\n    },\n    /**\n     * @private\n     */\n    _updateWishlistView: function () {\n        const $wishButton = $('.o_wsale_my_wish');\n        if ($wishButton.hasClass('o_wsale_my_wish_hide_empty')) {\n            $wishButton.toggleClass('d-none', !this.wishlistProductIDs.length);\n        }\n        $wishButton.find('.my_wish_quantity').text(this.wishlistProductIDs.length);\n    },\n    /**\n     * @private\n     */\n    _removeWish: function (e, deferred_redirect) {\n        var tr = $(e.currentTarget).parents('tr');\n        var wish = tr.data('wish-id');\n        var product = tr.data('product-id');\n        var self = this;\n\n        rpc('/shop/wishlist/remove/' + wish).then(function () {\n            $(tr).hide();\n        });\n\n        this.wishlistProductIDs = this.wishlistProductIDs.filter((p) => p !== product);\n        sessionStorage.setItem('website_sale_wishlist_product_ids', JSON.stringify(this.wishlistProductIDs));\n        if (this.wishlistProductIDs.length === 0) {\n            if (deferred_redirect) {\n                deferred_redirect.then(function () {\n                    self._redirectNoWish();\n                });\n            }\n        }\n        this._updateWishlistView();\n    },\n    /**\n     * @private\n     */\n    _addOrMoveWish: function (e) {\n        var tr = $(e.currentTarget).parents('tr');\n        var product = tr.data('product-id');\n        $('.o_wsale_my_cart').removeClass('d-none');\n\n        if ($('#b2b_wish').is(':checked')) {\n            return this._addToCart(product, tr.find('add_qty').val() || 1);\n        } else {\n            var adding_deffered = this._addToCart(product, tr.find('add_qty').val() || 1);\n            this._removeWish(e, adding_deffered);\n            return adding_deffered;\n        }\n    },\n    /**\n     * @private\n     */\n    _addToCart: function (productID, qty) {\n        const $tr = this.$(`tr[data-product-id=\"${productID}\"]`);\n        const productTrackingInfo = $tr.data('product-tracking-info');\n        if (productTrackingInfo) {\n            productTrackingInfo.quantity = parseFloat(qty);\n            $tr.trigger('add_to_cart_event', [productTrackingInfo]);\n        }\n        const callService = this.call.bind(this)\n        return rpc(\"/shop/cart/update_json\", {\n            ...this._getCartUpdateJsonParams(productID, qty),\n            display: false,\n        }).then(function (data) {\n            wSaleUtils.updateCartNavBar(data);\n            wSaleUtils.showCartNotification(callService, data.notification_info);\n        });\n    },\n    /**\n     * Get the cart update params.\n     *\n     * @param {string} productId\n     * @param {string} qty\n     */\n    _getCartUpdateJsonParams(productId, qty) {\n        return {\n            product_id: parseInt(productId, 10),\n            add_qty: parseInt(qty, 10),\n            display: false,\n        };\n    },\n    /**\n     * @private\n     */\n    _redirectNoWish: function () {\n        window.location.href = '/shop/cart';\n    },\n\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onClickMyWish: function () {\n        if (this.wishlistProductIDs.length === 0) {\n            this._updateWishlistView();\n            this._redirectNoWish();\n            return;\n        }\n        window.location = '/shop/wishlist';\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onClickAddWish: function (ev) {\n        this._addNewProducts($(ev.currentTarget));\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onChangeVariant: function (ev) {\n        var $input = $(ev.target);\n        var $parent = $input.closest('.js_product');\n        var $el = $parent.find(\"[data-action='o_wishlist']\");\n        if (!this.wishlistProductIDs.includes(parseInt($input.val(), 10))) {\n            $el.prop(\"disabled\", false).removeClass('disabled').removeAttr('disabled');\n        } else {\n            $el.prop(\"disabled\", true).addClass('disabled').attr('disabled', 'disabled');\n        }\n        $el.data('product-product-id', parseInt($input.val(), 10));\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onChangeProduct: function (ev) {\n        var productID = ev.currentTarget.value;\n        var $el = $(ev.target).closest('.js_add_cart_variants').find(\"[data-action='o_wishlist']\");\n\n        if (!this.wishlistProductIDs.includes(parseInt(productID, 10))) {\n            $el.prop(\"disabled\", false).removeClass('disabled').removeAttr('disabled');\n        } else {\n            $el.prop(\"disabled\", true).addClass('disabled').attr('disabled', 'disabled');\n        }\n        $el.data('product-product-id', productID);\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onClickWishRemove: function (ev) {\n        this._removeWish(ev, false);\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onClickWishAdd: function (ev) {\n        if (ev.currentTarget.classList.contains('disabled')) {\n            ev.preventDefault();\n            return;\n        }\n        var self = this;\n        this.$('.wishlist-section .o_wish_add').addClass('disabled');\n        this._addOrMoveWish(ev).then(function () {\n            self.$('.wishlist-section .o_wish_add').removeClass('disabled');\n        });\n    },\n});\n", "/** @odoo-module **/\n\nimport VariantMixin from \"@website_sale_stock/js/variant_mixin\";\nimport \"@website_sale/js/website_sale\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nconst oldChangeCombinationStock = VariantMixin._onChangeCombinationStock;\n/**\n * Displays additional info messages regarding the product's\n * stock and the wishlist.\n *\n * @override\n */\nVariantMixin._onChangeCombinationStock = function (ev, $parent, combination) {\n    oldChangeCombinationStock.apply(this, arguments);\n    if (this.el.querySelector('.o_add_wishlist_dyn')) {\n        const messageEl = this.el.querySelector('div.availability_messages');\n        if (messageEl && !this.el.querySelector('#stock_wishlist_message')) {\n            messageEl.append(\n                renderToElement('website_sale_stock_wishlist.product_availability', combination) || ''\n            );\n        }\n    }\n};\n", "/** @odoo-module **/\n\nimport WebsiteSale from '@website_sale_stock/js/website_sale';\n\nWebsiteSale.include({\n\n    events: Object.assign({}, WebsiteSale.prototype.events, {\n        'click #wishlist_stock_notification_message': '_onClickWishlistStockNotificationMessage',\n        'click #wishlist_stock_notification_form_submit_button': '_onClickSubmitWishlistStockNotificationForm',\n    }),\n\n    _onClickWishlistStockNotificationMessage(ev) {\n        this._handleClickStockNotificationMessage(ev);\n    },\n\n    _onClickSubmitWishlistStockNotificationForm(ev) {\n        const productId = ev.currentTarget.closest('tr').dataset.productId;\n        this._handleClickSubmitStockNotificationForm(ev, productId);\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport '@website_sale_wishlist/js/website_sale_wishlist';\n\npublicWidget.registry.ProductWishlist.include({\n\n    /**\n     * Removes wishlist indication when adding a product to the wishlist.\n     *\n     * @override\n     */\n    _addNewProducts: function () {\n        this._super(...arguments);\n        const saveForLaterButtonEl = document.querySelector('#wsale_save_for_later_button');\n        const addedToYourWishListAlertEl = document.querySelector('#wsale_added_to_your_wishlist_alert');\n        if (saveForLaterButtonEl) {\n            saveForLaterButtonEl.classList.add('d-none');\n            addedToYourWishListAlertEl.classList.remove('d-none');\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport { renderToElement } from \"@web/core/utils/render\";\nimport Fullscreen from \"@website_slides/js/slides_course_fullscreen_player\";\n\nFullscreen.include({\n    /**\n     * Extend the _renderSlide method so that slides of category \"certification\"\n     * are also taken into account and rendered correctly\n     *\n     * @private\n     * @override\n     */\n    _renderSlide: function () {\n        var def = this._super.apply(this, arguments);\n        const contentEl = this.el.querySelector(\".o_wslides_fs_content\");\n        if (this._slideValue.category === \"certification\") {\n            contentEl.innerHTML = \"\";\n            contentEl.append(\n                renderToElement(\"website.slides.fullscreen.certification\", { widget: this })\n            );\n        }\n        return Promise.all([def]);\n    },\n});\n", "/** @odoo-module **/\n\nimport { SlideUploadCategory } from \"@website_slides/js/public/components/slide_upload_dialog/slide_upload_category\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { onWillStart } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\n\npatch(SlideUploadCategory.prototype, {\n    setup() {\n        super.setup();\n        this.state.choices.certifications = [];\n        this.state.choices.certificationId = \"\";\n        this.state.showCertificationRequiredError = false;\n        onWillStart(async () => {\n            const results = await rpc(\"/slides_survey/certification/search_read\", {\n                fields: [\"title\"],\n            });\n\n            this.state.choices.certifications = results.read_results.map((certification) => {\n                return { value: certification.id, label: certification.title };\n            });\n        });\n    },\n\n    get displayCertificationValue() {\n        return this.state.choices.certificationId\n            ? this.state.choices.certifications.find(\n                  (c) => c.value === this.state.choices.certificationId\n              ).label\n            : _t(\"Select a certification\");\n    },\n\n    _formValidate() {\n        const isFormValid = super._formValidate();\n        if (this.props.slideCategory === \"certification\" && !this.state.choices.certificationId) {\n            this.state.showCertificationRequiredError = true;\n            return false;\n        }\n        return isFormValid;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    onCertificationSelect(value) {\n        this.state.choices.certificationId = value;\n        this.state.form.slideName = this.state.choices.certifications.find(\n            (c) => c.value === value\n        ).label;\n        this.state.showCertificationRequiredError = false;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _getSelectMenuValues() {\n        const result = super._getSelectMenuValues();\n        if (this.state.choices.certifications.length > 0) {\n            if (this._toCreate(this.state.choices.certificationId)) {\n                const certification = this.state.choices.certifications.find(\n                    (cert) => cert.value === this.state.choices.certificationId\n                );\n                result.survey = {\n                    id: false,\n                    title: certification.label,\n                };\n            } else {\n                result.survey = { id: parseInt(this.state.choices.certificationId, 10) };\n            }\n        }\n\n        return result;\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { SlideUploadDialog } from \"@website_slides/js/public/components/slide_upload_dialog/slide_upload_dialog\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(SlideUploadDialog.prototype, {\n    /**\n     * Overridden to add the \"certification\" slide category\n     */\n    setup() {\n        super.setup();\n        this.pagesTemplates[\"certification\"] =\n            \"website_slides_survey.SlideCategoryTutorial.Certification\";\n        this.slideCategoryData[\"certification\"] = {\n            icon: \"fa-trophy\",\n            label: _t(\"Certification\"),\n        };\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Overridden to add certifications management\n     * @param {String} slideCategory\n     */\n    onClickSlideCategoryIcon(slideCategory) {\n        super.onClickSlideCategoryIcon(slideCategory);\n        if (slideCategory === \"certification\") {\n            this.state.title = _t(\"Add Certification\");\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport { sprintf, escape } from \"@web/core/utils/strings\";\nimport { scrollTo } from \"@web_editor/js/common/scrolling\";\n\nexport function share(el, options) {\n    const option = {\n        shareLink: \"http://twitter.com/intent/tweet?text=\",\n        minLength: 5,\n        maxLength: 140,\n        target: \"blank\",\n        className: \"share\",\n        placement: \"top\",\n        ...options\n    };\n    let selectedText = \"\";\n\n    function init(shareable) {\n        shareable.addEventListener(\"mouseup\", () => {\n            if (!shareable.closest(\"body.editor_enable\")) {\n                popOver();\n            }\n        });\n        shareable.addEventListener(\"mousedown\", destroy);\n    }\n\n    function getContent() {\n        const popoverContentEl = document.createElement(\"div\");\n        popoverContentEl.className = \"h4 m-0\";\n\n        if (\n            document.querySelector(\n                \".o_wblog_title.js_comment, .o_wblog_post_content_field.js_comment\"\n            )\n        ) {\n            selectedText = getSelection(\"string\");\n            const btnEl = document.createElement(\"a\");\n            btnEl.className = \"o_share_comment btn btn-link px-2\";\n            btnEl.href = \"#\";\n            const iEl = document.createElement(\"i\");\n            iEl.className = \"fa fa-lg fa-comment\";\n            btnEl.appendChild(iEl);\n            popoverContentEl.appendChild(btnEl);\n        }\n\n        if (\n            document.querySelector(\".o_wblog_title.js_tweet, .o_wblog_post_content_field.js_tweet\")\n        ) {\n            const tweet = '\"%s\" - %s';\n            const baseLength = tweet.replace(/%s/g, \"\").length;\n            const selectedTextShort = getSelection(\"string\").substring(\n                0,\n                option.maxLength - baseLength - 23\n            );\n\n            const text = window.btoa(\n                encodeURIComponent(sprintf(tweet, selectedTextShort, window.location.href))\n            );\n\n            const anchorEL = document.createElement(\"a\");\n            anchorEL.href = \"#\";\n            anchorEL.classList.add(\"btn\");\n            anchorEL.addEventListener(\"click\", () => {\n                const decodedText = atob(text);\n                window.open(\n                    escape(option.shareLink) + decodedText,\n                    `_${escape(option.target)}`,\n                    \"location=yes,height=570,width=520,scrollbars=yes,status=yes\"\n                );\n            });\n            const iconEl = document.createElement(\"i\");\n            iconEl.className = \"ml4 mr4 fa fa-twitter fa-lg\";\n            anchorEL.appendChild(iconEl);\n            popoverContentEl.appendChild(anchorEL);\n        }\n\n        return popoverContentEl;\n    }\n\n    function commentEdition() {\n        const textareaEl = document.querySelector(\".o_portal_chatter_composer_body textarea\");\n        if (textareaEl) {\n            textareaEl.value = `\"${selectedText}\" `;\n            textareaEl.focus();\n        }\n        const commentsEl = document.getElementById(\"o_wblog_post_comments\");\n        if (commentsEl) {\n            scrollTo(commentsEl).then(() => {\n                window.location.hash = \"blog_post_comment_quote\";\n            });\n        }\n    }\n\n    function getSelection(type) {\n        const selection = window.getSelection();\n        if (!selection || selection.rangeCount === 0) {\n            return \"\";\n        }\n        if (type === \"string\") {\n            return String(selection.getRangeAt(0)).replace(/\\s{2,}/g, \" \");\n        } else {\n            return selection.getRangeAt(0);\n        }\n    }\n\n    function popOver() {\n        destroy();\n        if (getSelection(\"string\").length < option.minLength) {\n            return;\n        }\n        const data = getContent();\n        const range = getSelection();\n\n        const newNode = document.createElement(\"span\");\n        range.insertNode(newNode);\n        newNode.className = option.className;\n\n        const popover = Popover.getOrCreateInstance(newNode, {\n            trigger: \"manual\",\n            placement: option.placement,\n            html: true,\n            content: () => data,\n        });\n\n        popover.show();\n\n        const shareCommentEl = document.querySelector(\".o_share_comment\");\n        shareCommentEl?.addEventListener(\"click\", commentEdition);\n    }\n\n    function destroy() {\n        const spanEl = document.querySelector(`span.${option.className}`);\n        if (spanEl) {\n            const popover = Popover.getInstance(spanEl);\n            if (popover) {\n                popover.hide();\n            }\n            spanEl.remove();\n        }\n    }\n\n    init(el);\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { scrollTo } from \"@web_editor/js/common/scrolling\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { share } from \"./contentshare\";\n\npublicWidget.registry.websiteBlog = publicWidget.Widget.extend({\n    selector: '.website_blog',\n    events: {\n        'click #o_wblog_next_container': '_onNextBlogClick',\n        'click #o_wblog_post_content_jump': '_onContentAnchorClick',\n        'click .o_twitter, .o_facebook, .o_linkedin, .o_google, .o_twitter_complete, .o_facebook_complete, .o_linkedin_complete, .o_google_complete': '_onShareArticle',\n    },\n\n    /**\n     * @override\n     */\n    start: function () {\n        document.querySelectorAll(\".js_tweet, .js_comment\").forEach((el) => {\n            share(el);\n        });\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onNextBlogClick: function (ev) {\n        ev.preventDefault();\n        const nexInfo = ev.currentTarget.querySelector(\"#o_wblog_next_post_info\").dataset;\n        const recordCoverContainerEl = ev.currentTarget.querySelector(\".o_record_cover_container\");\n        const classes = nexInfo.size.split(\" \");\n        recordCoverContainerEl.classList.add(...classes, nexInfo.textContent);\n        ev.currentTarget.querySelectorAll(\".o_wblog_toggle\").forEach(el => el.classList.toggle(\"d-none\"));\n        // Appending a placeholder so that the cover can scroll to the top of the\n        // screen, regardless of its height.\n        const placeholder = document.createElement('div');\n        placeholder.style.minHeight = '100vh';\n        this.el.querySelector(\"#o_wblog_next_container\").append(placeholder);\n\n        // Use setTimeout() to calculate the 'offset()'' only after that size classes\n        // have been applyed and that $el has been resized.\n        setTimeout(() => {\n            this._forumScrollAction(ev.currentTarget, 300, function () {\n                window.location.href = nexInfo.url;\n            });\n        });\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onContentAnchorClick: function (ev) {\n        ev.preventDefault();\n        ev.stopImmediatePropagation();\n        const currentTargetEl = document.querySelector(ev.currentTarget.hash);\n\n        this._forumScrollAction(currentTargetEl, 500, function () {\n            window.location.hash = 'blog_content';\n        });\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onShareArticle: function (ev) {\n        ev.preventDefault();\n        let url = \"\";\n        const blogPostTitle = document.querySelector(\"#o_wblog_post_name\").textContent || \"\";\n        const articleURL = window.location.href;\n        if (ev.currentTarget.classList.contains(\"o_twitter\")) {\n            const tweetText = _t(\"Amazing blog article: %(title)s! Check it live: %(url)s\", {\n                title: blogPostTitle,\n                url: articleURL,\n            });\n            url = 'https://twitter.com/intent/tweet?tw_p=tweetbutton&text=' + encodeURIComponent(tweetText);\n        } else if (ev.currentTarget.classList.contains(\"o_facebook\")) {\n            url = 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(articleURL);\n        } else if (ev.currentTarget.classList.contains(\"o_linkedin\")) {\n            url = 'https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent(articleURL);\n        }\n        window.open(url, '', 'menubar=no, width=500, height=400');\n    },\n\n    //--------------------------------------------------------------------------\n    // Utils\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {HTMLElement} el - the element we are scrolling to\n     * @param {Integer} duration - scroll animation duration\n     * @param {Function} callback - to be executed after the scroll is performed\n     */\n    _forumScrollAction: function (el, duration, callback) {\n        scrollTo(el, { duration: duration }).then(() => callback());\n    },\n});\n", "/** @odoo-module **/\n\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npublicWidget.registry.websiteEventMeetingRoom = publicWidget.Widget.extend({\n    selector: '.o_wevent_meeting_room_card',\n    events: {\n        'click .o_wevent_meeting_room_delete': '_onDeleteClick',\n        'click .o_wevent_meeting_room_duplicate': '_onDuplicateClick',\n        'click .o_wevent_meeting_room_is_pinned': '_onPinClick',\n    },\n\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n    },\n\n    start: function () {\n        this._super.apply(this, arguments);\n        this.meetingRoomId = parseInt(this.el.dataset[\"meetingRoomId\"]);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n      * Delete the meeting room.\n      *\n      * @private\n      */\n    _onDeleteClick: async function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n\n        this.call(\"dialog\", \"add\", ConfirmationDialog, {\n            body: _t(\"Are you sure you want to close this room?\"),\n            confirm: async () => {\n                await this.orm.write(\n                    \"event.meeting.room\",\n                    [this.meetingRoomId],\n                    { is_published: false },\n                    { context: this.context }\n                );\n\n                // remove the element so we do not need to refresh the page\n                this.el.remove();\n            },\n        });\n    },\n\n    /**\n      * Duplicate the room.\n      *\n      * @private\n      */\n    _onDuplicateClick: function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n        this.call(\"dialog\", \"add\", ConfirmationDialog, {\n            body: _t(\"Are you sure you want to duplicate this room?\"),\n            confirm: async () => {\n                await this.orm.call(\"event.meeting.room\", \"copy\", [this.meetingRoomId], {\n                    context: this.context,\n                });\n\n                window.location.reload();\n            },\n        });\n    },\n\n    /**\n      * Pin/unpin the room.\n      *\n      * @private\n      */\n    _onPinClick: async function (event) {\n        event.preventDefault();\n        event.stopPropagation();\n\n        const pinnedButtonClass = \"o_wevent_meeting_room_pinned\";\n        const isPinned = event.currentTarget.classList.contains(pinnedButtonClass);\n\n        await this.orm.write(\n            \"event.meeting.room\",\n            [this.meetingRoomId],\n            { is_pinned: !isPinned },\n            { context: this.context }\n        );\n\n        // TDE FIXME: addclass ?\n        if (isPinned) {\n            event.currentTarget.classList.remove(pinnedButtonClass);\n        } else {\n            event.currentTarget.classList.add(pinnedButtonClass);\n        }\n    }\n});\n\nexport default publicWidget.registry.websiteEventMeetingRoom;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\npublicWidget.registry.websiteEventCreateMeetingRoom = publicWidget.Widget.extend({\n    selector: '.o_wevent_create_room_button',\n    events: {\n        'click': '_onClickCreate',\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    _onClickCreate: async function () {\n        if (!this.createModalEl) {\n            const langs = await rpc(\"/event/active_langs\");\n\n            this.createModalEl = renderToElement(\"event_meet_create_room_modal\", {\n                csrf_token: odoo.csrf_token,\n                eventId: this.el.dataset.eventId,\n                defaultLangCode: this.el.dataset.defaultLangCode,\n                langs: langs,\n            });\n            this.el.parentNode.append(this.createModalEl);\n        }\n\n        Modal.getOrCreateInstance(this.createModalEl).show();\n    },\n\n    //--------------------------------------------------------------------------\n    // Override\n    //--------------------------------------------------------------------------\n\n    /**\n     * Remove the create modal from the DOM, to avoid issue when editing the template\n     * with the website editor.\n     *\n     * @override\n     */\n    destroy: function () {\n        const modalEl = document.querySelector(\".o_wevent_create_meeting_room_modal\");\n        if (modalEl) {\n            modalEl.remove();\n        }\n        this._super.apply(this, arguments);\n    },\n});\n\nexport default publicWidget.registry.websiteEventMeetingRoom;\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { ObservingCookieWidgetMixin } from \"@website/snippets/observing_cookie_mixin\";\n\n// Note that Instagram can automatically detect the language of the user and\n// translate the embed.\n\nconst InstagramPage = publicWidget.Widget.extend(ObservingCookieWidgetMixin, {\n    selector: \".s_instagram_page\",\n    disabledInEditableMode: false,\n\n    /**\n     * @override\n     */\n    start() {\n        const iframeEl = document.createElement(\"iframe\");\n        this.el.querySelector(\".o_instagram_container\").appendChild(iframeEl);\n        iframeEl.setAttribute(\"scrolling\", \"no\");\n        iframeEl.setAttribute(\"aria-label\", _t(\"Instagram\"));\n        iframeEl.classList.add(\"w-100\");\n        // We can already estimate the height of the iframe.\n        iframeEl.height = this._estimateIframeHeight();\n        // We have to setup the message listener before setting the src, because\n        // the iframe can send a message before this JS is fully loaded.\n        this.__onMessage = this._onMessage.bind(this);\n        window.addEventListener(\"message\", this.__onMessage);\n\n        // We set the src now, we are ready to receive the message.\n        const src = `https://www.instagram.com/${this.el.dataset.instagramPage}/embed`;\n        this._manageIframeSrc(this.el, src);\n\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        const iframeEl = this.el.querySelector(\".o_instagram_container iframe\");\n        if (iframeEl) {\n            iframeEl.remove();\n            window.removeEventListener(\"message\", this.__onMessage);\n        }\n        this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Gives an estimation of the height of the Instagram iframe.\n     *\n     * @private\n     * @returns {number}\n     */\n    _estimateIframeHeight() {\n        // In the meantime Instagram doesn't send us a message with the height,\n        // we use a formula to estimate the height of the iframe (the formula\n        // has been found with a linear regression).\n        const iframeEl = this.el.querySelector(\".o_instagram_container iframe\");\n        const iframeWidth = parseInt(getComputedStyle(iframeEl).width);\n        // The profile picture is smaller when width < 432px.\n        return 0.659 * iframeWidth + (iframeWidth < 432 ? 156 : 203);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when a message is sent. Instagram sends us a message with the\n     * height of the iframe.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onMessage(ev) {\n        const iframeEl = this.el.querySelector(\".o_instagram_container iframe\");\n        if (!iframeEl) {\n            // TODO: fix this case. We should never end up here. It happens when\n            // - Drop Instagram snippet\n            // - Undo\n            // - Drop a new Instagram snippet\n            // => The listener of the first one is still active because the\n            // public widget has not been destroyed.\n            window.removeEventListener(\"message\", this.__onMessage);\n            return;\n        }\n        if (ev.origin !== \"https://www.instagram.com\" || iframeEl.contentWindow !== ev.source) {\n            // It's not a message from Instagram or it's a message from another\n            // Instagram iframe.\n            return;\n        }\n        const evDataJSON = JSON.parse(ev.data);\n        if (evDataJSON.type !== \"MEASURE\") {\n            // It's not a measure message.\n            return;\n        }\n        const height = parseInt(evDataJSON.details.height);\n        // Here we get the exact height of the iframe.\n        // Instagram can return a height of 0 before the real height.\n        if (height) {\n            // Prevent history step in edit mode.\n            this.options.wysiwyg?.odooEditor.observerUnactive();\n            iframeEl.height = height;\n            this.options.wysiwyg?.odooEditor.observerActive();\n        }\n    },\n});\n\npublicWidget.registry.InstagramPage = InstagramPage;\n\nexport default InstagramPage;\n", "/** @odoo-module */\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\n\nconst ShareWidget = publicWidget.Widget.extend({\n    selector: '.s_share, .oe_share', // oe_share for compatibility\n    events: {\n        'click a': '_onShareLinkClick',\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Everything is done on click here (even changing the href) as the URL we\n     * want to share may be updated during the page use (like when updating\n     * variant on a product page then clicking on a share link).\n     *\n     * @private\n     */\n    _onShareLinkClick(ev) {\n        const urlParams = [\"u\", \"url\", \"body\"];\n        const titleParams = [\"title\", \"text\", \"subject\", \"description\"];\n        const mediaParams = [\"media\"];\n        const aEl = ev.currentTarget;\n        // We don't modify the original URL in case the user clicks again on the\n        // sharer later.\n        const modifiedUrl = new URL(aEl.href);\n\n        // Try and support old use of share snippet as a social link snippet:\n        // if the URL does not look like a sharer, then do nothing. This\n        // obviously won't cover all cases (people may have added URL that look\n        // like sharer but are not but in that case, it was probably already\n        // broken before).\n        if (![...urlParams, ...titleParams, ...mediaParams]\n                .some(param => modifiedUrl.searchParams.has(param))) {\n            return;\n        }\n\n        ev.preventDefault();\n        ev.stopPropagation();\n\n        // We don't need to encode the URL as searchParams.set does it for us.\n        const currentUrl = window.location.href;\n\n        const urlParamFound = urlParams.find(param => modifiedUrl.searchParams.has(param));\n        if (urlParamFound) {\n            modifiedUrl.searchParams.set(urlParamFound, currentUrl);\n        }\n\n        const titleParamFound = titleParams.find(param => modifiedUrl.searchParams.has(param));\n        if (titleParamFound) {\n            // We don't need to encode the title as searchParams.set does it.\n            const currentTitle = document.title;\n            if (aEl.classList.contains('s_share_whatsapp')) {\n                // WhatsApp does not support the \"url\" GET parameter.\n                // Instead we need to include the url within the passed \"text\"\n                // parameter, merging everything together, e.g of output:\n                // https://wa.me/?text=%20OpenWood%20Collection%20Online%20Reveal%20%7C%20My%20Website%20http%3A%2F%2Flocalhost%3A8888%2Fevent%2Fopenwood-collection-online-reveal-2021-06-21-2021-06-23-8%2Fregister\n                // For more details, see https://faq.whatsapp.com/general/chats/how-to-use-click-to-chat/\n                modifiedUrl.searchParams.set(titleParamFound, `${currentTitle} ${currentUrl}`);\n            } else {\n                // The built-in `URLSearchParams.set()` method encodes spaces\n                // as \"+\" characters, which are not properly parsed as spaces\n                // by email clients, so we can't use it here.\n                modifiedUrl.search = modifiedUrl.search\n                    .replace(encodeURIComponent(\"{title}\"), encodeURIComponent(currentTitle));\n            }\n        }\n\n        const mediaParamFound = mediaParams.find(param => modifiedUrl.searchParams.has(param));\n        if (mediaParamFound) {\n            const ogImageEl = document.querySelector(\"meta[property='og:image']\");\n            // Some pages (/profile/user/ID) don't have an image to share.\n            if (ogImageEl) {\n                // We don't need to encode the media as searchParams does it.\n                const media = ogImageEl.content;\n                modifiedUrl.searchParams.set(mediaParamFound, media);\n            } else {\n                modifiedUrl.searchParams.delete(mediaParamFound);\n            }\n        }\n\n        window.open(\n            modifiedUrl.toString(),\n            aEl.target,\n            \"menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=550,width=600\",\n        );\n    },\n});\n\npublicWidget.registry.share = ShareWidget;\n\nexport default ShareWidget;\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { clamp } from \"@web/core/utils/numbers\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { ObservingCookieWidgetMixin } from \"@website/snippets/observing_cookie_mixin\";\n\nconst FacebookPageWidget = publicWidget.Widget.extend(ObservingCookieWidgetMixin, {\n    selector: '.o_facebook_page',\n    disabledInEditableMode: false,\n\n    /**\n     * @override\n     */\n    start: function () {\n        var def = this._super.apply(this, arguments);\n        this.previousWidth = 0;\n\n        const params = pick(this.$el[0].dataset, 'href', 'id', 'height', 'tabs', 'small_header', 'hide_cover');\n        if (!params.href) {\n            return def;\n        }\n        if (params.id) {\n            params.href = `https://www.facebook.com/${params.id}`;\n        }\n        delete params.id;\n\n        this._renderIframe(params);\n        this.resizeObserver = new ResizeObserver(debounce(this._renderIframe.bind(this, params), 100));\n        this.resizeObserver.observe(this.el.parentElement);\n\n        return def;\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        this._super.apply(this, arguments);\n        if (this.iframeEl) {\n            this._deactivateEditorObserver();\n            this.iframeEl.remove();\n            this._activateEditorObserver();\n            this.resizeObserver.disconnect();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Prepare iframe element & replace it with existing iframe.\n     *\n     * @private\n     * @param {Object} params\n    */\n    _renderIframe(params) {\n        this._deactivateEditorObserver();\n\n        params.width = clamp(Math.floor(this.$el.width()), 180, 500);\n        if (this.previousWidth !== params.width) {\n            this.previousWidth = params.width;\n            const searchParams = new URLSearchParams(params);\n            const src = \"https://www.facebook.com/plugins/page.php?\" + searchParams;\n            this.iframeEl = Object.assign(document.createElement(\"iframe\"), {\n                scrolling: \"no\",\n            });\n            // TODO: remove, the \"scrolling\", \"frameborder\" and\n            // \"allowTransparency\" attributes in master as they are deprecated.\n            // Also put the width and height as iframe attribute.\n            this.iframeEl.setAttribute(\"frameborder\", \"0\");\n            this.iframeEl.setAttribute(\"allowTransparency\", \"true\");\n            this.iframeEl.setAttribute(\"style\", `width: ${params.width}px; height: ${params.height}px; border: none; overflow: hidden;`);\n            this.iframeEl.setAttribute(\"aria-label\", _t(\"Facebook\"));\n            this.el.replaceChildren(this.iframeEl);\n            this._manageIframeSrc(this.el, src);\n        }\n\n        this._activateEditorObserver();\n    },\n\n    /**\n     * Activates the editor observer if it exists.\n     */\n    _activateEditorObserver() {\n        this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerActive();\n    },\n\n    /**\n     * Deactivates the editor observer if it exists.\n     */\n    _deactivateEditorObserver() {\n        this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerUnactive();\n    },\n});\n\npublicWidget.registry.facebookPage = FacebookPageWidget;\n\nexport default FacebookPageWidget;\n", "/** @odoo-module **/\n\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\n\nconst GalleryWidget = publicWidget.Widget.extend({\n\n    selector: '.s_image_gallery:not(.o_slideshow)',\n    events: {\n        'click img': '_onClickImg',\n    },\n\n    /**\n     * @override\n     */\n    start() {\n        this._super(...arguments);\n        this.originalSources = [...this.el.querySelectorAll(\"img\")].map(img => img.getAttribute(\"src\"));\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when an image is clicked. Opens a dialog to browse all the images\n     * with a bigger size.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onClickImg: function (ev) {\n        const clickedEl = ev.currentTarget;\n        if (this.$modal || clickedEl.matches(\"a > img\")) {\n            return;\n        }\n        var self = this;\n\n        let imageEls = this.el.querySelectorAll(\"img\");\n        const currentImageEl = clickedEl.closest(\"img\");\n        const currentImageIndex = [...imageEls].indexOf(currentImageEl);\n        // We need to reset the images to their original source because it might\n        // have been changed by a mouse event (e.g. \"hover effect\" animation).\n        imageEls = [...imageEls].map((el, i) => {\n            const cloneEl = el.cloneNode(true);\n            cloneEl.src = this.originalSources[i];\n            return cloneEl;\n        });\n\n        var size = 0.8;\n        var dimensions = {\n            min_width: Math.round(window.innerWidth * size * 0.9),\n            min_height: Math.round(window.innerHeight * size),\n            max_width: Math.round(window.innerWidth * size * 0.9),\n            max_height: Math.round(window.innerHeight * size),\n            width: Math.round(window.innerWidth * size * 0.9),\n            height: Math.round(window.innerHeight * size)\n        };\n\n        const milliseconds = this.el.dataset.interval || false;\n        const lightboxTemplate = this.$target[0].dataset.vcss === \"002\" ?\n            \"website.gallery.s_image_gallery_mirror.lightbox\" :\n            \"website.gallery.slideshow.lightbox\";\n        this.$modal = $(renderToElement(lightboxTemplate, {\n            images: imageEls,\n            index: currentImageIndex,\n            dim: dimensions,\n            interval: milliseconds || 0,\n            ride: !milliseconds ? \"false\" : \"carousel\",\n            id: uniqueId(\"slideshow_\"),\n        }));\n        this.__onModalKeydown = this._onModalKeydown.bind(this);\n        this.$modal.on('hidden.bs.modal', function () {\n            $(this).hide();\n            $(this).siblings().filter('.modal-backdrop').remove(); // bootstrap leaves a modal-backdrop\n            this.removeEventListener(\"keydown\", self.__onModalKeydown);\n            $(this).remove();\n            self.$modal = undefined;\n        });\n        this.$modal.one('shown.bs.modal', function () {\n            self.trigger_up('widgets_start_request', {\n                editableMode: false,\n                $target: self.$modal.find('.modal-body.o_slideshow'),\n            });\n            this.addEventListener(\"keydown\", self.__onModalKeydown);\n        });\n        this.$modal.appendTo(document.body);\n        const modalBS = new Modal(this.$modal[0], {keyboard: true, backdrop: true});\n        modalBS.show();\n    },\n    _onModalKeydown(ev) {\n        if (ev.key === \"ArrowLeft\" || ev.key === \"ArrowRight\") {\n            const side = ev.key === \"ArrowLeft\" ? \"prev\" : \"next\";\n            this.$modal[0].querySelector(`.carousel-control-${side}`).click();\n        }\n        if (ev.key === \"Escape\") {\n            // If the user is connected as an editor, prevent the backend header\n            // from collapsing.\n            ev.stopPropagation();\n        }\n    },\n});\n\nconst GallerySliderWidget = publicWidget.Widget.extend({\n    selector: '.o_slideshow',\n    disabledInEditableMode: false,\n\n    /**\n     * @override\n     */\n    start: function () {\n        var self = this;\n        this.$carousel = this.$el.is('.carousel') ? this.$el : this.$('.carousel');\n        this.$indicator = this.$carousel.find('.carousel-indicators');\n        this.$prev = this.$indicator.find('li.o_indicators_left').css('visibility', ''); // force visibility as some databases have it hidden\n        this.$next = this.$indicator.find('li.o_indicators_right').css('visibility', '');\n        var $lis = this.$indicator.find('li[data-bs-slide-to]');\n        let indicatorWidth = this.$indicator.width();\n        if (indicatorWidth === 0) {\n            // An ancestor may be hidden so we try to find it and make it\n            // visible just to take the correct width.\n            const $indicatorParent = this.$indicator.parents().not(':visible').last();\n            if (!$indicatorParent[0].style.display) {\n                $indicatorParent[0].style.display = 'block';\n                indicatorWidth = this.$indicator.width();\n                $indicatorParent[0].style.display = '';\n            }\n        }\n        let nbPerPage = Math.floor(indicatorWidth / $lis.first().outerWidth(true)) - 3; // - navigator - 1 to leave some space\n        var realNbPerPage = nbPerPage || 1;\n        var nbPages = Math.ceil($lis.length / realNbPerPage);\n\n        var index;\n        var page;\n        update();\n\n        function hide() {\n            $lis.each(function (i) {\n                $(this).toggleClass('d-none', i < page * nbPerPage || i >= (page + 1) * nbPerPage);\n            });\n            if (page <= 0) {\n                self.$prev.detach();\n            } else {\n                self.$prev.removeClass('d-none');\n                self.$prev.prependTo(self.$indicator);\n            }\n            if (page >= nbPages - 1) {\n                self.$next.detach();\n            } else {\n                self.$next.removeClass('d-none');\n                self.$next.appendTo(self.$indicator);\n            }\n        }\n\n        function update() {\n            const active = $lis.filter('.active');\n            index = active.length ? $lis.index(active) : 0;\n            page = Math.floor(index / realNbPerPage);\n            hide();\n        }\n\n        this.$carousel.on('slide.bs.carousel.gallery_slider', function () {\n            setTimeout(function () {\n                var $item = self.$carousel.find('.carousel-inner .carousel-item-prev, .carousel-inner .carousel-item-next');\n                var index = $item.index();\n                $lis.removeClass('active')\n                    .filter('[data-bs-slide-to=\"' + index + '\"]')\n                    .addClass('active');\n            }, 0);\n        });\n        this.$indicator.on('click.gallery_slider', '> li:not([data-bs-slide-to])', function () {\n            page += ($(this).hasClass('o_indicators_left') ? -1 : 1);\n            page = Math.max(0, Math.min(nbPages - 1, page)); // should not be necessary\n            self.$carousel.carousel(page * realNbPerPage);\n            // We dont use hide() before the slide animation in the editor because there is a traceback\n            // TO DO: fix this traceback\n            if (!self.editableMode) {\n                hide();\n            }\n        });\n        this.$carousel.on('slid.bs.carousel.gallery_slider', update);\n\n        return this._super.apply(this, arguments);\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        this._super.apply(this, arguments);\n\n        if (!this.$indicator) {\n            return;\n        }\n\n        this.$prev.prependTo(this.$indicator);\n        this.$next.appendTo(this.$indicator);\n        this.$carousel.off('.gallery_slider');\n        this.$indicator.off('.gallery_slider');\n    },\n});\n\npublicWidget.registry.gallery = GalleryWidget;\npublicWidget.registry.gallerySlider = GallerySliderWidget;\n\nexport default {\n    GalleryWidget: GalleryWidget,\n    GallerySliderWidget: GallerySliderWidget,\n};\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport weUtils from \"@web_editor/js/common/utils\";\nimport { isCSSColor } from '@web/core/utils/colors';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nconst CountdownWidget = publicWidget.Widget.extend({\n    selector: '.s_countdown',\n    disabledInEditableMode: false,\n\n    /**\n     * @override\n     */\n    start: function () {\n        // Remove SVG previews (used to simulated canvas)\n        this.$el[0].querySelectorAll('svg').forEach(el => {\n            el.parentNode.remove();\n        });\n\n        this.$wrapper = this.$('.s_countdown_canvas_wrapper');\n        this.$wrapper.addClass('d-flex justify-content-center');\n        this.hereBeforeTimerEnds = false;\n        this.endAction = this.el.dataset.endAction;\n        this.endTime = parseInt(this.el.dataset.endTime);\n        this.size = parseInt(this.el.dataset.size);\n        this.display = this.el.dataset.display;\n        if (!this.display && this.el.dataset.bsDisplay) {\n            // With the BS5 upgrade script of 16.0, countdowns' data-display may\n            // have been converted to data-bs-display by mistake. This will fix\n            // the DOM for good measures, maybe even allowing to remove this\n            // code in a few years as hopefully all current countdowns will have\n            // been removed or edited (or when a proper upgrade script in a\n            // future version of Odoo will be made, if necessary). TODO.\n            this.display = this.el.dataset.bsDisplay;\n            delete this.el.dataset.bsDisplay;\n            this.el.dataset.display = this.display;\n        }\n\n        this.layout = this.el.dataset.layout;\n        this.layoutBackground = this.el.dataset.layoutBackground;\n        this.progressBarStyle = this.el.dataset.progressBarStyle;\n        this.progressBarWeight = this.el.dataset.progressBarWeight;\n\n        this.textColor = this._ensureCssColor(this.el.dataset.textColor);\n        this.layoutBackgroundColor = this._ensureCssColor(this.el.dataset.layoutBackgroundColor);\n        this.progressBarColor = this._ensureCssColor(this.el.dataset.progressBarColor);\n\n        this.onlyOneUnit = this.display === 'd';\n        this.width = parseInt(this.size);\n        if (this.layout === 'boxes') {\n            this.width /= 1.75;\n        }\n        this._initTimeDiff();\n\n        this._render();\n\n        this.setInterval = setInterval(this._render.bind(this), 1000);\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        this.$('.s_countdown_end_redirect_message').remove();\n        this.$('.s_countdown_end_message').addClass('d-none');\n        this.$('.s_countdown_text_wrapper').remove();\n        this.$('.s_countdown_canvas_wrapper').removeClass('d-none');\n        this.$('.s_countdown_canvas_flex').remove();\n\n        clearInterval(this.setInterval);\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Ensures the color is an actual css color. In case of a color variable,\n     * the color will be mapped to hexa.\n     *\n     * @private\n     * @param {string} color\n     * @returns {string}\n     */\n    _ensureCssColor: function (color) {\n        if (isCSSColor(color)) {\n            return color;\n        }\n        return weUtils.getCSSVariableValue(color) || this.defaultColor;\n    },\n    /**\n     * Gets the time difference in seconds between now and countdown due date.\n     *\n     * @private\n     */\n    _getDelta: function () {\n        const currentTimestamp = Date.now() / 1000;\n        return this.endTime - currentTimestamp;\n    },\n    /**\n     * Handles the action that should be executed once the countdown ends.\n     *\n     * @private\n     */\n    _handleEndCountdownAction: function () {\n        if (this.endAction === 'redirect') {\n            const redirectUrl = this.el.dataset.redirectUrl || '/';\n            if (this.hereBeforeTimerEnds) {\n                // Wait a bit, if the landing page has the same publish date\n                setTimeout(() => window.location = redirectUrl, 500);\n            } else {\n                // Show (non editable) msg when user lands on already finished countdown\n                if (!this.$('.s_countdown_end_redirect_message').length) {\n                    const $container = this.$('> .container, > .container-fluid, > .o_container_small');\n                    $container.append(\n                        $(renderToElement('website.s_countdown.end_redirect_message', {\n                            redirectUrl: redirectUrl,\n                        }))\n                    );\n                }\n            }\n        } else if (this.endAction === 'message' || this.endAction === 'message_no_countdown') {\n            this.$('.s_countdown_end_message').removeClass('d-none');\n        }\n    },\n    /**\n     * Initializes the `diff` object. It will contains every visible time unit\n     * which will each contain its related canvas, total step, label..\n     *\n     * @private\n     */\n    _initTimeDiff: function () {\n        const delta = this._getDelta();\n        this.diff = [];\n        if (this._isUnitVisible('d') && !(this.onlyOneUnit && delta < 86400)) {\n            this.diff.push({\n                canvas: $('<div class=\"s_countdown_canvas_flex\"><canvas class=\"w-100\"/></div>').appendTo(this.$wrapper)[0],\n                // There is no logical number of unit (total) on which day units\n                //  can be compared against, so we use an arbitrary number.\n                total: 15,\n                label: _t(\"Days\"),\n                nbSeconds: 86400,\n            });\n        }\n        if (this._isUnitVisible('h') || (this.onlyOneUnit && delta < 86400 && delta > 3600)) {\n            this.diff.push({\n                canvas: $('<div class=\"s_countdown_canvas_flex\"><canvas class=\"w-100\"/></div>').appendTo(this.$wrapper)[0],\n                total: 24,\n                label: _t(\"Hours\"),\n                nbSeconds: 3600,\n            });\n        }\n        if (this._isUnitVisible('m') || (this.onlyOneUnit && delta < 3600 && delta > 60)) {\n            this.diff.push({\n                canvas: $('<div class=\"s_countdown_canvas_flex\"><canvas class=\"w-100\"/></div>').appendTo(this.$wrapper)[0],\n                total: 60,\n                label: _t(\"Minutes\"),\n                nbSeconds: 60,\n            });\n        }\n        if (this._isUnitVisible('s') || (this.onlyOneUnit && delta < 60)) {\n            this.diff.push({\n                canvas: $('<div class=\"s_countdown_canvas_flex\"><canvas class=\"w-100\"/></div>').appendTo(this.$wrapper)[0],\n                total: 60,\n                label: _t(\"Seconds\"),\n                nbSeconds: 1,\n            });\n        }\n    },\n    /**\n     * Returns weither or not the countdown should be displayed for the given\n     * unit (days, sec..).\n     *\n     * @private\n     * @param {string} unit - either 'd', 'm', 'h', or 's'\n     * @returns {boolean}\n     */\n    _isUnitVisible: function (unit) {\n        return this.display.includes(unit);\n    },\n    /**\n     * Draws the whole countdown, including one countdown for each time unit.\n     *\n     * @private\n     */\n    _render: function () {\n\n        // If only one unit mode, restart widget on unit change to populate diff\n        if (this.onlyOneUnit && this._getDelta() < this.diff[0].nbSeconds) {\n            this.$('.s_countdown_canvas_flex').remove();\n            this._initTimeDiff();\n        }\n        this._updateTimeDiff();\n\n        const hideCountdown = this.isFinished && !this.editableMode && this.$el.hasClass('hide-countdown');\n        if (this.layout === 'text') {\n            this.$('.s_countdown_canvas_flex').addClass('d-none');\n            if (!this.$textWrapper) {\n                this.$textWrapper = $('<span/>').attr({\n                    class: 's_countdown_text_wrapper d-none',\n                });\n                this.$textWrapper.text(_t(\"Countdown ends in\"));\n                this.$textWrapper.append($('<span/>').attr({\n                    class: 's_countdown_text ms-1',\n                }));\n                this.$textWrapper.appendTo(this.$wrapper);\n            }\n\n            this.$textWrapper.toggleClass('d-none', hideCountdown);\n\n            const countdownText = this.diff.map(e => e.nb + ' ' + e.label).join(', ');\n            this.$('.s_countdown_text').text(countdownText.toLowerCase());\n        } else {\n            for (const val of this.diff) {\n                const canvas = val.canvas.querySelector('canvas');\n                const ctx = canvas.getContext(\"2d\");\n                ctx.canvas.width = this.width;\n                ctx.canvas.height = this.size;\n                this._clearCanvas(ctx);\n\n                $(canvas).toggleClass('d-none', hideCountdown);\n                if (hideCountdown) {\n                    continue;\n                }\n\n                // Draw canvas elements\n                if (this.layoutBackground !== 'none') {\n                    this._drawBgShape(ctx, this.layoutBackground === 'plain');\n                }\n                this._drawText(canvas, val.nb, val.label, this.layoutBackground === 'plain');\n                if (this.progressBarStyle === 'surrounded') {\n                    this._drawProgressBarBg(ctx, this.progressBarWeight === 'thin');\n                }\n                if (this.progressBarStyle !== 'none') {\n                    this._drawProgressBar(ctx, val.nb, val.total, this.progressBarWeight === 'thin');\n                }\n                this.$('.s_countdown_canvas_flex').toggleClass('mx-1', this.layout === 'boxes');\n            }\n        }\n\n        if (this.isFinished) {\n            clearInterval(this.setInterval);\n            if (!this.editableMode) {\n                this._handleEndCountdownAction();\n            }\n        }\n    },\n    /**\n     * Updates the remaining units into the `diff` object.\n     *\n     * @private\n     */\n    _updateTimeDiff: function () {\n        let delta = this._getDelta();\n        this.isFinished = delta < 0;\n        if (this.isFinished) {\n            for (const unitData of this.diff) {\n                  unitData.nb = 0;\n            }\n            return;\n        }\n\n        this.hereBeforeTimerEnds = true;\n        for (const unitData of this.diff) {\n              unitData.nb = Math.floor(delta / unitData.nbSeconds);\n              delta -= unitData.nb * unitData.nbSeconds;\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Canvas drawing methods\n    //--------------------------------------------------------------------------\n\n    /**\n     * Erases the canvas.\n     *\n     * @private\n     * @param {RenderingContext} ctx - Context of the canvas\n     */\n    _clearCanvas: function (ctx) {\n        ctx.clearRect(0, 0, this.size, this.size);\n    },\n    /**\n     * Draws a text into the canvas.\n     *\n     * @private\n     * @param {HTMLCanvasElement} canvas\n     * @param {string} textNb - text to display in the center of the canvas, in big\n     * @param {string} textUnit - text to display bellow `textNb` in small\n     * @param {boolean} full - if true, the shape will be drawn up to the progressbar\n     */\n    _drawText: function (canvas, textNb, textUnit, full = false) {\n        const ctx = canvas.getContext(\"2d\");\n        const nbSize = this.size / 4;\n        ctx.font = `${nbSize}px Arial`;\n        ctx.fillStyle = this.textColor;\n        ctx.textAlign = 'center';\n        ctx.textBaseline = 'middle';\n        ctx.fillText(textNb, canvas.width / 2, canvas.height / 2);\n\n        const unitSize = this.size / 12;\n        ctx.font = `${unitSize}px Arial`;\n        ctx.fillText(textUnit, canvas.width / 2, canvas.height / 2 + nbSize / 1.5, this.width);\n\n        if (this.layout === 'boxes' && this.layoutBackground !== 'none' && this.progressBarStyle === 'none') {\n            let barWidth = this.size / (this.progressBarWeight === 'thin' ? 31 : 10);\n            if (full) {\n                barWidth = 0;\n            }\n            ctx.beginPath();\n            ctx.moveTo(barWidth, this.size / 2);\n            ctx.lineTo(this.width - barWidth, this.size / 2);\n            ctx.stroke();\n        }\n    },\n    /**\n     * Draws a plain shape into the canvas.\n     *\n     * @private\n     * @param {RenderingContext} ctx - Context of the canvas\n     * @param {boolean} full - if true, the shape will be drawn up to the progressbar\n     */\n    _drawBgShape: function (ctx, full = false) {\n        ctx.fillStyle = this.layoutBackgroundColor;\n        ctx.beginPath();\n        if (this.layout === 'circle') {\n            let rayon = this.size / 2;\n            if (this.progressBarWeight === 'thin') {\n                rayon -= full ? this.size / 29 : this.size / 15;\n            } else {\n                rayon -= full ? 0 : this.size / 10;\n            }\n            ctx.arc(this.size / 2, this.size / 2, rayon, 0, Math.PI * 2);\n            ctx.fill();\n        } else if (this.layout === 'boxes') {\n            let barWidth = this.size / (this.progressBarWeight === 'thin' ? 31 : 10);\n            if (full) {\n                barWidth = 0;\n            }\n\n            ctx.fillStyle = this.layoutBackgroundColor;\n            ctx.rect(barWidth, barWidth, this.width - barWidth * 2, this.size - barWidth * 2);\n            ctx.fill();\n\n            const gradient = ctx.createLinearGradient(0, this.width, 0, 0);\n            gradient.addColorStop(0, '#ffffff24');\n            gradient.addColorStop(1, this.layoutBackgroundColor);\n            ctx.fillStyle = gradient;\n            ctx.rect(barWidth, barWidth, this.width - barWidth * 2, this.size - barWidth * 2);\n            ctx.fill();\n            $(ctx.canvas).css({'border-radius': '8px'});\n        }\n    },\n    /**\n     * Draws a progress bar around the countdown shape.\n     *\n     * @private\n     * @param {RenderingContext} ctx - Context of the canvas\n     * @param {string} nbUnit - how many unit should fill progress bar\n     * @param {string} totalUnit - number of unit to do a complete progress bar\n     * @param {boolean} thinLine - if true, the progress bar will be thiner\n     */\n    _drawProgressBar: function (ctx, nbUnit, totalUnit, thinLine) {\n        ctx.strokeStyle = this.progressBarColor;\n        ctx.lineWidth = thinLine ? this.size / 35 : this.size / 10;\n        if (this.layout === 'circle') {\n            ctx.beginPath();\n            ctx.arc(this.size / 2, this.size / 2, this.size / 2 - this.size / 20, Math.PI / -2, (Math.PI * 2) * (nbUnit / totalUnit) + (Math.PI / -2));\n            ctx.stroke();\n        } else if (this.layout === 'boxes') {\n            ctx.lineWidth *= 2;\n            let pc = nbUnit / totalUnit * 100;\n\n            // Lines: Top(x1,y1,x2,y2) Right(x1,y1,x2,y2) Bottom(x1,y1,x2,y2) Left(x1,y1,x2,y2)\n            const linesCoordFuncs = [\n                (linePc) => [0 + ctx.lineWidth / 2, 0, (this.width - ctx.lineWidth / 2) * linePc / 25 + ctx.lineWidth / 2, 0],\n                (linePc) => [this.width, 0 + ctx.lineWidth / 2, this.width, (this.size - ctx.lineWidth / 2) * linePc / 25 + ctx.lineWidth / 2],\n                (linePc) => [this.width - ((this.width - ctx.lineWidth / 2) * linePc / 25) - ctx.lineWidth / 2, this.size, this.width - ctx.lineWidth / 2, this.size],\n                (linePc) => [0, this.size - ((this.size - ctx.lineWidth / 2) * linePc / 25) - ctx.lineWidth / 2, 0, this.size - ctx.lineWidth / 2],\n            ];\n            while (pc > 0 && linesCoordFuncs.length) {\n                const linePc = Math.min(pc, 25);\n                const lineCoord = (linesCoordFuncs.shift())(linePc);\n                ctx.beginPath();\n                ctx.moveTo(lineCoord[0], lineCoord[1]);\n                ctx.lineTo(lineCoord[2], lineCoord[3]);\n                ctx.stroke();\n                pc -= linePc;\n            }\n        }\n    },\n    /**\n     * Draws a full lighter background progressbar around the shape.\n     *\n     * @private\n     * @param {RenderingContext} ctx - Context of the canvas\n     * @param {boolean} thinLine - if true, the progress bar will be thiner\n     */\n    _drawProgressBarBg: function (ctx, thinLine) {\n        ctx.strokeStyle = this.progressBarColor;\n        ctx.globalAlpha = 0.2;\n        ctx.lineWidth = thinLine ? this.size / 35 : this.size / 10;\n        if (this.layout === 'circle') {\n            ctx.beginPath();\n            ctx.arc(this.size / 2, this.size / 2, this.size / 2 - this.size / 20, 0, Math.PI * 2);\n            ctx.stroke();\n        } else if (this.layout === 'boxes') {\n            ctx.lineWidth *= 2;\n\n            // Lines: Top(x1,y1,x2,y2) Right(x1,y1,x2,y2) Bottom(x1,y1,x2,y2) Left(x1,y1,x2,y2)\n            const points = [\n                [0 + ctx.lineWidth / 2, 0, this.width, 0],\n                [this.width, 0 + ctx.lineWidth / 2, this.width, this.size],\n                [0, this.size, this.width - ctx.lineWidth / 2, this.size],\n                [0, 0, 0, this.size - ctx.lineWidth / 2],\n            ];\n            while (points.length) {\n                const point = points.shift();\n                ctx.beginPath();\n                ctx.moveTo(point[0], point[1]);\n                ctx.lineTo(point[2], point[3]);\n                ctx.stroke();\n            }\n        }\n        ctx.globalAlpha = 1;\n    },\n});\n\npublicWidget.registry.countdown = CountdownWidget;\n\nexport default CountdownWidget;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport {throttleForAnimation} from \"@web/core/utils/timing\";\nimport { isVisible } from \"@web/core/utils/ui\";\nimport { utils as uiUtils, MEDIAS_BREAKPOINTS, SIZES } from \"@web/core/ui/ui_service\";\nimport {setUtmsHtmlDataset} from '@website/js/content/inject_dom';\nimport wUtils from \"@website/js/utils\";\nimport { ObservingCookieWidgetMixin } from \"@website/snippets/observing_cookie_mixin\";\n\n// TODO In master, export this class too or merge it with PopupWidget\nconst SharedPopupWidget = publicWidget.Widget.extend({\n    selector: '.s_popup',\n    disabledInEditableMode: false,\n    events: {\n        // A popup element is composed of a `.s_popup` parent containing the\n        // actual `.modal` BS modal. Our internal logic and events are hiding\n        // and showing this inner `.modal` modal element without considering its\n        // `.s_popup` parent. It means that when the `.modal` is hidden, its\n        // `.s_popup` parent is not touched and kept visible.\n        // It might look like it's not an issue as it would just be an empty\n        // element (its only child is hidden) but it leads to some issues as for\n        // instance on chrome this div will have a forced `height` due to its\n        // `contenteditable=true` attribute in edit mode. It will result in a\n        // ugly white bar.\n        // tl;dr: this is keeping those 2 elements visibility synchronized.\n        'show.bs.modal': '_onModalShow',\n        'hidden.bs.modal': '_onModalHidden',\n    },\n\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n\n        // Popup are always closed when entering edit mode (see PopupWidget),\n        // this allows to make sure the class is sync on the .s_popup parent\n        // after that moment too.\n        if (!this.editableMode) {\n            this.el.classList.add('d-none');\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onModalShow() {\n        this.el.classList.remove('d-none');\n    },\n    /**\n     * @private\n     */\n    _onModalHidden() {\n        if (this.el.querySelector('.s_popup_no_backdrop')) {\n            // We trigger a scroll event here to call the\n            // '_hideBottomFixedElements' method and re-display any bottom fixed\n            // elements that may have been hidden (e.g. the live chat button\n            // hidden when the cookies bar is open).\n            $().getScrollingTarget()[0].dispatchEvent(new Event('scroll'));\n        }\n\n        this.el.classList.add('d-none');\n    },\n});\n\npublicWidget.registry.SharedPopup = SharedPopupWidget;\n\nconst PopupWidget = publicWidget.Widget.extend(ObservingCookieWidgetMixin, {\n    selector: \".s_popup:not(#website_cookies_bar)\",\n    events: {\n        'click .js_close_popup': '_onCloseClick',\n        'click .btn-primary': '_onBtnPrimaryClick',\n        'hide.bs.modal': '_onHideModal',\n        'show.bs.modal': '_onShowModal',\n    },\n    cookieValue: true,\n\n    /**\n     * @override\n     */\n    start: function () {\n        this.modalShownOnClickEl = this.el.querySelector(\".modal[data-display='onClick']\");\n        if (this.modalShownOnClickEl) {\n            // We add a \"hashchange\" listener in case a button to open a popup\n            // is clicked.\n            this.__onHashChange = this._onHashChange.bind(this);\n            window.addEventListener('hashchange', this.__onHashChange);\n            // Check if a hash exists and if the modal needs to be opened when\n            // the page loads (e.g. The user has clicked a button on the\n            // \"Contact us\" page to open a popup on the homepage).\n            this._showPopupOnClick();\n        } else {\n            this._popupAlreadyShown = !!cookie.get(this.$el.attr('id'));\n            // Check if every child element of the popup is conditionally hidden,\n            // and if so, never show an empty popup.\n            // config.device.isMobile is true if the device is <= SM, but the device\n            // visibility option uses < LG to hide on mobile. So compute it here.\n            const isMobile = uiUtils.getSize() < SIZES.LG;\n            const emptyPopup = [\n                ...this.$el[0].querySelectorAll(\".oe_structure > *:not(.s_popup_close)\")\n            ].every((el) => {\n                const visibilitySelectors = el.dataset.visibilitySelectors;\n                const deviceInvisible = isMobile\n                    ? el.classList.contains(\"o_snippet_mobile_invisible\")\n                    : el.classList.contains(\"o_snippet_desktop_invisible\");\n                return (visibilitySelectors && el.matches(visibilitySelectors)) || deviceInvisible;\n            });\n            if (!this._popupAlreadyShown && !emptyPopup) {\n                this._bindPopup();\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        this._super.apply(this, arguments);\n        $(document).off('mouseleave.open_popup');\n        this.$el.find('.modal').modal('hide');\n        clearTimeout(this.timeout);\n        if (this.modalShownOnClickEl) {\n            window.removeEventListener('hashchange', this.__onHashChange);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _bindPopup: function () {\n        const $main = this.$el.find('.modal');\n\n        let display = $main.data('display');\n        let delay = $main.data('showAfter');\n\n        if (uiUtils.isSmall()) {\n            if (display === 'mouseExit') {\n                display = 'afterDelay';\n                delay = 5000;\n            }\n        }\n\n        if (display === 'afterDelay') {\n            this.timeout = setTimeout(() => this._showPopup(), delay);\n        } else if (display === \"mouseExit\") {\n            $(document).on('mouseleave.open_popup', () => this._showPopup());\n        }\n    },\n    /**\n     * @private\n     */\n    _canShowPopup() {\n        return true;\n    },\n    /**\n     * @private\n     */\n    _hidePopup: function () {\n        this.$el.find('.modal').modal('hide');\n    },\n    /**\n     * @private\n     */\n    _showPopup: function () {\n        if (this._popupAlreadyShown || !this._canShowPopup()) {\n            return;\n        }\n        this.$el.find('.modal').modal('show');\n    },\n    /**\n     * @private\n     */\n    _showPopupOnClick(hash = window.location.hash) {\n        // If a hash exists in the URL and it corresponds to the ID of the modal,\n        // then we open the modal.\n        if (hash && hash.substring(1) === this.modalShownOnClickEl.id) {\n            // We remove the hash from the URL because otherwise the popup\n            // cannot open again after being closed.\n            const urlWithoutHash = window.location.href.replace(hash, '');\n            window.history.replaceState(null, null, urlWithoutHash);\n            this._showPopup();\n        }\n    },\n    /**\n     * Checks if the given primary button should allow or not to close the\n     * modal.\n     *\n     * @private\n     * @param {HTMLElement} primaryBtnEl\n     */\n    _canBtnPrimaryClosePopup(primaryBtnEl) {\n        return !(\n            primaryBtnEl.classList.contains(\"s_website_form_send\")\n            || primaryBtnEl.classList.contains(\"o_website_form_send\")\n        );\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onCloseClick: function () {\n        this._hidePopup();\n    },\n    /**\n     * @private\n     */\n    _onBtnPrimaryClick(ev) {\n        if (this._canBtnPrimaryClosePopup(ev.target)) {\n            this._hidePopup();\n        }\n    },\n    /**\n     * @private\n     */\n    _onHideModal: function () {\n        const nbDays = this.$el.find('.modal').data('consentsDuration');\n        cookie.set(this.el.id, this.cookieValue, nbDays * 24 * 60 * 60, 'required');\n        this._popupAlreadyShown = true && !this.modalShownOnClickEl;\n\n        this.$el.find('.media_iframe_video iframe').each((i, iframe) => {\n            iframe.src = '';\n        });\n    },\n    /**\n     * @private\n     */\n    _onShowModal() {\n        this.el.querySelectorAll('.media_iframe_video').forEach(media => {\n            // TODO still oeExpression to remove someday\n            this._manageIframeSrc(media, media.dataset.oeExpression || media.dataset.src);\n        });\n    },\n    /**\n     * @private\n     */\n    _onHashChange(ev) {\n        // Keep the new hash from the event to avoid conflict with the eCommerce\n        // hash attributes managing.\n        // TODO : it should not have been a hash at all for ecommerce, but a\n        // query string parameter\n        this._showPopupOnClick(new URL(ev.newURL).hash);\n    },\n});\n\npublicWidget.registry.popup = PopupWidget;\n\nconst noBackdropPopupWidget = publicWidget.Widget.extend({\n    selector: '.s_popup_no_backdrop',\n    disabledInEditableMode: false,\n    events: {\n        'shown.bs.modal': '_onModalNoBackdropShown',\n        'hide.bs.modal': '_onModalNoBackdropHide',\n    },\n\n    /**\n     * @override\n     */\n    start() {\n        this.throttledUpdateScrollbar = throttleForAnimation(() => this._updateScrollbar());\n        if (this.editableMode && this.el.classList.contains('show')) {\n            // Use case: When the \"Backdrop\" option is disabled in edit mode.\n            // The page scrollbar must be adjusted and events must be added.\n            this._updateScrollbar();\n            this._addModalNoBackdropEvents();\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        this._removeModalNoBackdropEvents();\n        // After destroying the widget, we need to trigger a resize event so that\n        // the scrollbar can adjust to its default behavior.\n        window.dispatchEvent(new Event('resize'));\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _updateScrollbar() {\n        // When there is no backdrop the element with the scrollbar is\n        // '.modal-content' (see comments in CSS).\n        const modalContent = this.el.querySelector('.modal-content');\n        const isOverflowing = $(modalContent).hasScrollableContent();\n        const modalInstance = window.Modal.getInstance(this.el);\n        if (isOverflowing) {\n            // If the \"no-backdrop\" modal has a scrollbar, the page's scrollbar\n            // must be hidden. This is because if the two scrollbars overlap, it\n            // is no longer possible to scroll using the modal's scrollbar.\n            modalInstance._adjustDialog();\n        } else {\n            // If the \"no-backdrop\" modal does not have a scrollbar, the page\n            // scrollbar must be displayed because we must be able to scroll the\n            // page (e.g. a \"cookies bar\" popup at the bottom of the page must\n            // not prevent scrolling the page).\n            modalInstance._resetAdjustments();\n        }\n    },\n    /**\n     * @private\n     */\n    _addModalNoBackdropEvents() {\n        window.addEventListener('resize', this.throttledUpdateScrollbar);\n        this.resizeObserver = new window.ResizeObserver(() => {\n            // When the size of the modal changes, the scrollbar needs to be\n            // adjusted.\n            this._updateScrollbar();\n        });\n        this.resizeObserver.observe(this.el.querySelector('.modal-content'));\n    },\n    /**\n     * @private\n     */\n    _removeModalNoBackdropEvents() {\n        this.throttledUpdateScrollbar.cancel();\n        window.removeEventListener('resize', this.throttledUpdateScrollbar);\n        if (this.resizeObserver) {\n            this.resizeObserver.disconnect();\n            delete this.resizeObserver;\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onModalNoBackdropShown() {\n        this._updateScrollbar();\n        this._addModalNoBackdropEvents();\n    },\n    /**\n     * @private\n     */\n    _onModalNoBackdropHide() {\n        this._removeModalNoBackdropEvents();\n    },\n});\n\npublicWidget.registry.noBackdropPopup = noBackdropPopupWidget;\n\n// Extending the popup widget with cookiebar functionality.\n// This allows for refusing optional cookies for now and can be\n// extended to picking which cookies categories are accepted.\npublicWidget.registry.cookies_bar = PopupWidget.extend({\n    selector: '#website_cookies_bar',\n    events: Object.assign({}, PopupWidget.prototype.events, {\n        'click #cookies-consent-essential, #cookies-consent-all': '_onAcceptClick',\n        \"show_cookies_bar\": \"_onShowCookiesBar\",\n    }),\n\n    /**\n     * @override\n     */\n    destroy() {\n        if (this.toggleEl) {\n            this.toggleEl.removeEventListener(\"click\", this._onToggleCookiesBar);\n            this.toggleEl.remove();\n        }\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _showPopup() {\n        this._super(...arguments);\n        const policyLinkEl = this.el.querySelector(\".o_cookies_bar_text_policy\");\n        if (policyLinkEl && window.location.pathname === new URL(policyLinkEl.href).pathname) {\n            this.toggleEl = wUtils.cloneContentEls(`\n            <button class=\"o_cookies_bar_toggle btn btn-info btn-sm rounded-circle d-flex gap-2 align-items-center position-fixed pe-auto\">\n                <i class=\"fa fa-eye\" alt=\"\" aria-hidden=\"true\"></i> <span class=\"o_cookies_bar_toggle_label\"></span>\n            </button>\n            `).firstElementChild;\n            this.el.insertAdjacentElement(\"beforebegin\", this.toggleEl);\n            this._toggleCookiesBar();\n            this._onToggleCookiesBar = this._toggleCookiesBar.bind(this);\n            this.toggleEl.addEventListener(\"click\", this._onToggleCookiesBar);\n        }\n    },\n    /**\n     * Toggles the cookies bar with a button so that the page is readable.\n     *\n     * @private\n     */\n    _toggleCookiesBar() {\n        const popupEl = this.el.querySelector(\".modal\");\n        $(popupEl).modal(\"toggle\");\n        // As we're using Bootstrap's events, the PopupWidget prevents the modal\n        // from being shown after hiding it: override that behavior.\n        this._popupAlreadyShown = false;\n        cookie.delete(this.el.id);\n\n        const hidden = !popupEl.classList.contains(\"show\");\n        this.toggleEl.querySelector(\".fa\").className = `fa ${hidden ? \"fa-eye\" : \"fa-eye-slash\"}`;\n        this.toggleEl.querySelector(\".o_cookies_bar_toggle_label\").innerText = hidden\n            ? _t(\"Show the cookies bar\")\n            : _t(\"Hide the cookies bar\");\n        if (hidden || !popupEl.classList.contains(\"s_popup_bottom\")) {\n            this.toggleEl.style.removeProperty(\"--cookies-bar-toggle-inset-block-end\");\n        } else {\n            // Lazy-loaded images don't have a height yet. We need to await them\n            wUtils.onceAllImagesLoaded($(popupEl)).then(() => {\n                const popupHeight = popupEl.querySelector(\".modal-content\").offsetHeight;\n                const toggleMargin = 8;\n                // Avoid having the toggleEl over another button, but if the\n                // cookies bar is too tall, place it at the bottom anyway.\n                const bottom = document.body.offsetHeight > popupHeight + this.toggleEl.offsetHeight + toggleMargin\n                    ? `calc(\n                        ${getComputedStyle(popupEl.querySelector(\".modal-dialog\")).paddingBottom}\n                        + ${popupHeight + toggleMargin}px\n                    )`\n                    : \"\";\n                this.toggleEl.style.setProperty(\"--cookies-bar-toggle-inset-block-end\", bottom);\n            });\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param ev\n     */\n    _onAcceptClick(ev) {\n        const isFullConsent = ev.target.id === \"cookies-consent-all\";\n        this.cookieValue = `{\"required\": true, \"optional\": ${isFullConsent}}`;\n        if (isFullConsent) {\n            document.dispatchEvent(new Event(\"optionalCookiesAccepted\"));\n        }\n        this._onHideModal();\n        this.toggleEl && this.toggleEl.remove();\n    },\n    /**\n     * @override\n     */\n    _onHideModal() {\n        this._super(...arguments);\n        const params = new URLSearchParams(window.location.search);\n        const trackingFields = {\n            utm_campaign: \"odoo_utm_campaign\",\n            utm_source: \"odoo_utm_source\",\n            utm_medium: \"odoo_utm_medium\",\n        };\n        for (const [key, value] of params) {\n            if (key in trackingFields) {\n                // Using same cookie expiration value as in python side\n                cookie.set(trackingFields[key], value, 31 * 24 * 60 * 60, \"optional\");\n            }\n        }\n        setUtmsHtmlDataset();\n    },\n    /**\n     * Reopens the cookies bar if it was closed.\n     *\n     * @private\n     */\n    _onShowCookiesBar() {\n        const modalEl = this.el.querySelector(\".modal\");\n        const currCookie = cookie.get(this.el.id);\n\n        if (currCookie && JSON.parse(currCookie).optional || !this._popupAlreadyShown) {\n            return;\n        }\n        $(modalEl).modal(\"show\");\n\n        // The cookies bar remains hidden, most probably because of the browser\n        // or an extension: notify the user because \"nothing happens when I\n        // click\" is never good.\n        if (!isVisible(modalEl)) {\n            window.alert(_t(\"Our cookies bar was blocked by your browser or an extension.\"));\n            return;\n        }\n        modalEl.focus();\n    },\n});\n\npublicWidget.registry.CookiesApproval = publicWidget.Widget.extend({\n    selector: \"[data-need-cookies-approval]\",\n    events: {\n        \"add_cookies_warning\": \"_onAddCookiesWarning\",\n    },\n\n    /**\n     * @override\n     */\n    async start() {\n        this.iframeEl = this.el.tagName === \"IFRAME\" ? this.el : this.el.querySelector(\"iframe\");\n        if (this.iframeEl) {\n            this.optionalCookiesWarningEl = this.iframeEl.nextElementSibling\n                ?.classList.contains(\"o_no_optional_cookie\")\n                ? this.iframeEl.nextElementSibling\n                : null;\n            if (!this.optionalCookiesWarningEl) {\n                this._addOptionalCookiesWarning();\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        if (this._onWarningElClick && this._onRemoveOptionalCookiesWarning) {\n            this.optionalCookiesWarningEl.removeEventListener(\"click\", this._onWarningElClick);\n            document.removeEventListener(\"optionalCookiesAccepted\", this._onRemoveOptionalCookiesWarning);\n            this._removeOptionalCookiesWarning();\n        }\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Adds a warning in place of the iframe. On click, shows the cookies bar if\n     * it was hidden.\n     *\n     * @private\n     */\n    _addOptionalCookiesWarning() {\n        const options = {\n            extraStyle: this.iframeEl.parentElement.classList.contains(\"media_iframe_video\")\n                ? `aspect-ratio: 16/9; max-width: ${MEDIAS_BREAKPOINTS[SIZES.SM].maxWidth}px;`\n                : \"\",\n            extraClasses: getComputedStyle(this.iframeEl.parentElement).position === \"absolute\"\n                ? \"\" : \"my-3\",\n        };\n        this.optionalCookiesWarningEl = renderToElement(\"website.cookiesWarning\", options);\n        this.iframeEl.insertAdjacentElement(\"afterend\", this.optionalCookiesWarningEl);\n        this.iframeEl.classList.add(\"d-none\");\n\n        this._onWarningElClick = () => {\n            $(document.getElementById(\"website_cookies_bar\")).trigger(\"show_cookies_bar\");\n        };\n        this.optionalCookiesWarningEl.addEventListener(\"click\", this._onWarningElClick);\n        this._onRemoveOptionalCookiesWarning = this._removeOptionalCookiesWarning.bind(this);\n        document.addEventListener(\n            \"optionalCookiesAccepted\",\n            this._onRemoveOptionalCookiesWarning,\n            { once: true }\n        );\n    },\n    /**\n     * Removes the warning and attributes preventing the iframe from being shown\n     *\n     * @private\n     */\n    _removeOptionalCookiesWarning() {\n        this.iframeEl.src = this.iframeEl.dataset.nocookieSrc;\n        this.iframeEl.classList.remove(\"d-none\");\n        delete this.iframeEl.dataset.nocookieSrc;\n        delete this.iframeEl.dataset.needCookiesApproval;\n        delete this.iframeEl.closest(\":not(iframe)[data-need-cookies-approval]\")\n            ?.dataset.needCookiesApproval;\n        this.optionalCookiesWarningEl.remove();\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Triggers only on iframes created client-side (for which a widget has not\n     * been created): `this.el` is a parent to which the event propagates.\n     *\n     * @private\n     */\n    _onAddCookiesWarning(ev) {\n        ev.stopPropagation();\n        if (!ev.target.nextElementSibling?.classList.contains(\"o_no_optional_cookie\")) {\n            this.iframeEl = ev.target;\n            this._addOptionalCookiesWarning();\n        }\n    },\n});\n\nexport default PopupWidget;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport {extraMenuUpdateCallbacks} from \"@website/js/content/menu\";\nimport { closestScrollable } from \"@web_editor/js/common/scrolling\";\n\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';\nconst CLASS_NAME_ACTIVE = 'active';\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';\nconst SELECTOR_NAV_LINKS = '.nav-link';\nconst SELECTOR_NAV_ITEMS = '.nav-item';\nconst SELECTOR_LIST_ITEMS = '.list-group-item';\nconst SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}, .${CLASS_NAME_DROPDOWN_ITEM}`;\nconst SELECTOR_DROPDOWN = '.dropdown';\nconst SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle';\n\nconst getSelector = element => {\n    let hrefAttr = element.getAttribute('href');\n    if (!hrefAttr || !hrefAttr.startsWith('#')) {\n        return null;\n    }\n    const selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : null;\n    return selector;\n};\n\nconst parents = (element, selector) => {\n    const parents = [];\n    let ancestor = element.parentNode.closest(selector);\n    while (ancestor) {\n        parents.push(ancestor);\n        ancestor = ancestor.parentNode.closest(selector);\n    }\n    return parents;\n};\n\nconst prev = (element, selector) => {\n    let previous = element.previousElementSibling;\n    while (previous) {\n        if (previous.matches(selector)) {\n            return [previous];\n        }\n        previous = previous.previousElementSibling;\n    }\n    return [];\n};\n\nconst TableOfContent = publicWidget.Widget.extend({\n    selector: 'section .s_table_of_content_navbar_sticky',\n    disabledInEditableMode: false,\n\n    init() {\n        this._super(...arguments);\n        this._onScrollBound = this._process.bind(this);\n        this._offsets = [];\n        this._targets = [];\n        this._activeTarget = null;\n        this._scrollHeight = 0;\n        this._offset = 0;\n    },\n\n    /**\n     * @override\n     */\n    async start() {\n        this._stripNavbarStyles();\n        this._scrollElement = closestScrollable(this.$target.closest(\".s_table_of_content\")[0]);\n        this._scrollTarget = $().getScrollingTarget(this._scrollElement)[0];\n        this._tocElement = this.el.querySelector('.s_table_of_content_navbar');\n        this.previousPosition = -1;\n        this._updateTableOfContentNavbarPosition();\n        this._updateTableOfContentNavbarPositionBound = this._updateTableOfContentNavbarPosition.bind(this);\n        extraMenuUpdateCallbacks.push(this._updateTableOfContentNavbarPositionBound);\n        this._scrollTarget.addEventListener(\"scroll\", this._onScrollBound);\n        await this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._scrollTarget?.removeEventListener(\"scroll\", this._onScrollBound);\n        const indexCallback = extraMenuUpdateCallbacks.indexOf(this._updateTableOfContentNavbarPositionBound);\n        if (indexCallback >= 0) {\n            extraMenuUpdateCallbacks.splice(indexCallback, 1);\n        }\n        this.$el.css('top', '');\n        this.$el.find('.s_table_of_content_navbar').css({top: '', maxHeight: ''});\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _stripNavbarStyles() {\n        // This is needed for styles added on translations when the master text\n        // has no style.\n        for (let el of this.el.querySelectorAll('.s_table_of_content_navbar .table_of_content_link')) {\n            const translationEl = el.querySelector('span[data-oe-translation-state]');\n            if (translationEl) {\n                el = translationEl;\n            }\n            const text = el.textContent; // Get text from el.\n            el.textContent = text; // Replace all of el's content with that text.\n        }\n    },\n    /**\n     * @private\n     */\n    _updateTableOfContentNavbarPosition() {\n        if (!this.el.querySelector('a.table_of_content_link')) {\n            // Do not start the scrollspy if the TOC is empty.\n            return;\n        }\n        let position = 0;\n        const $fixedElements = $('.o_top_fixed_element');\n        $fixedElements.toArray().forEach((el) => position += $(el).outerHeight());\n        const isHorizontalNavbar = this.$el.hasClass('s_table_of_content_horizontal_navbar');\n        this.$el.css('top', isHorizontalNavbar ? position : '');\n        this.$el.find('.s_table_of_content_navbar').css('top', isHorizontalNavbar ? '' : position + 20);\n        position += isHorizontalNavbar ? this.$el.outerHeight() : 0;\n        this.$el.find('.s_table_of_content_navbar').css('maxHeight', isHorizontalNavbar ? '' : `calc(100vh - ${position + 40}px)`);\n        if (this.previousPosition !== position) {\n            this._offset = position + 100;\n            this._refresh();\n            this._process();\n            this.previousPosition = position;\n        }\n    },\n\n    _getScrollHeight() {\n        return this._scrollElement.scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);\n    },\n\n    _refresh() {\n        this._offsets = [];\n        this._targets = [];\n        this._scrollHeight = this._getScrollHeight();\n        const targets = [...this._tocElement.querySelectorAll(SELECTOR_LINK_ITEMS)];\n        targets.map(element => {\n            const targetSelector = getSelector(element);\n            const target = targetSelector ? document.querySelector(targetSelector) : null;\n            if (target) {\n                const targetBCR = target.getBoundingClientRect();\n\n                if (targetBCR.width || targetBCR.height) {\n                    return [targetBCR.top, targetSelector];\n                }\n            }\n            return null;\n        }).filter(item => item).sort((a, b) => a[0] - b[0]).forEach(item => {\n            this._offsets.push(item[0]);\n            this._targets.push(item[1]);\n        });\n        const baseScrollTop = this._scrollElement.scrollTop;\n        for (let i = 0; i < this._offsets.length; i++) {\n            this._offsets[i] += baseScrollTop;\n        }\n    },\n\n    _activate(target) {\n        const element = document.querySelector(`[href=\"${target}\"]`);\n        if (!element || $(element).is(':hidden')) {\n            return;\n        }\n        this._activeTarget = target;\n        this._clear();\n        const queries = SELECTOR_LINK_ITEMS.split(',').map(selector => `${selector}[href=\"${target}\"]`);\n        const link = this._tocElement.querySelector(queries.join(','));\n        link.classList.add(CLASS_NAME_ACTIVE);\n        if (link.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n            link.closest(SELECTOR_DROPDOWN).querySelector(SELECTOR_DROPDOWN_TOGGLE, link.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE);\n        } else {\n            parents(link, SELECTOR_NAV_LIST_GROUP).forEach(listGroup => {\n                // Set triggered links parents as active\n                // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor\n                prev(listGroup, `${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`).forEach(item => item.classList.add(CLASS_NAME_ACTIVE)); // Handle special case when .nav-link is inside .nav-item\n                prev(listGroup, SELECTOR_NAV_ITEMS).forEach(navItem => {\n                    [...navItem.children].filter(child => child.matches(SELECTOR_NAV_LINKS)).forEach(item => item.classList.add(CLASS_NAME_ACTIVE));\n                });\n            });\n        }\n    },\n\n    _clear() {\n        [...this._tocElement.querySelectorAll(SELECTOR_LINK_ITEMS)].filter(node => node.classList.contains(CLASS_NAME_ACTIVE)).forEach(node => node.classList.remove(CLASS_NAME_ACTIVE));\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    _process() {\n        const scrollTop = this._scrollElement.scrollTop + this._offset;\n        const scrollHeight = this._getScrollHeight();\n        const maxScroll = this._offset + scrollHeight - this._scrollElement.getBoundingClientRect().height;\n        if (this._scrollHeight !== scrollHeight) {\n            this._refresh();\n        }\n        if (scrollTop >= maxScroll) {\n            const target = this._targets[this._targets.length - 1];\n            if (this._activeTarget !== target) {\n                this._activate(target);\n            }\n            return;\n        }\n        if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {\n            this._activeTarget = null;\n            this._clear();\n        } else {\n            for (let i = this._offsets.length; i--;) {\n                const isActiveTarget = this._activeTarget !== this._targets[i] && scrollTop >= this._offsets[i] && (typeof this._offsets[i + 1] === 'undefined' || scrollTop < this._offsets[i + 1]);\n\n                if (isActiveTarget) {\n                    this._activate(this._targets[i]);\n                }\n            }\n        }\n        if (this._activeTarget === null) {\n            this._activate(this._targets[0]);\n        }\n    },\n});\n\npublicWidget.registry.anchorSlide.include({\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Overridden to add the height of the horizontal sticky navbar at the scroll value\n     * when the link is from the table of content navbar\n     *\n     * @override\n     * @private\n     */\n    _computeExtraOffset() {\n        let extraOffset = this._super(...arguments);\n        if (this.$el.hasClass('table_of_content_link')) {\n            const tableOfContentNavbarEl = this.$el.closest('.s_table_of_content_navbar_sticky.s_table_of_content_horizontal_navbar');\n            if (tableOfContentNavbarEl.length > 0) {\n                extraOffset += $(tableOfContentNavbarEl).outerHeight();\n            }\n        }\n        return extraOffset;\n    },\n});\n\npublicWidget.registry.snippetTableOfContent = TableOfContent;\n\nexport default TableOfContent;\n", "/** @odoo-module **/\n\nimport { loadBundle } from \"@web/core/assets\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport weUtils from \"@web_editor/js/common/utils\";\n\nconst ChartWidget = publicWidget.Widget.extend({\n    selector: '.s_chart',\n    disabledInEditableMode: false,\n\n    /**\n     * @override\n     * @param {Object} parent\n     * @param {Object} options The default value of the chartbar.\n     */\n    init: function (parent, options) {\n        this._super.apply(this, arguments);\n        this.style = window.getComputedStyle(document.documentElement);\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        // Convert Theme colors to css color\n        const data = JSON.parse(this.el.dataset.data);\n        data.datasets.forEach(el => {\n            if (Array.isArray(el.backgroundColor)) {\n                el.backgroundColor = el.backgroundColor.map(el => this._convertToCssColor(el));\n                el.borderColor = el.borderColor.map(el => this._convertToCssColor(el));\n            } else {\n                el.backgroundColor = this._convertToCssColor(el.backgroundColor);\n                el.borderColor = this._convertToCssColor(el.borderColor);\n            }\n            el.borderWidth = this.el.dataset.borderWidth;\n        });\n\n        const radialAxis = {\n            beginAtZero: true,\n        };\n\n        const linearAxis = {\n            type: \"linear\",\n            stacked: this.el.dataset.stacked === \"true\",\n            beginAtZero: true,\n            min: parseInt(this.el.dataset.ticksMin),\n            max: parseInt(this.el.dataset.ticksMax),\n        };\n\n        const categoryAxis = {\n            type: \"category\",\n        };\n\n        // Make chart data\n        const chartData = {\n            type: this.el.dataset.type,\n            data: data,\n            options: {\n                plugins: {\n                    legend: {\n                        display: this.el.dataset.legendPosition !== 'none',\n                        position: this.el.dataset.legendPosition,\n                    },\n                    tooltip: {\n                        enabled: this.el.dataset.tooltipDisplay === 'true',\n                        position: \"custom\",\n                    },\n                    title: {\n                        display: !!this.el.dataset.title,\n                        text: this.el.dataset.title,\n                    },\n                },\n                scales: {\n                    x: categoryAxis,\n                    y: linearAxis,\n                },\n                aspectRatio: 2,\n            },\n        };\n\n        // Add type specific options\n        if (this.el.dataset.type === 'radar') {\n            chartData.options.scales = {\n                r: radialAxis,\n            };\n        } else if (this.el.dataset.type === \"horizontalBar\") {\n            chartData.type = \"bar\";\n            chartData.options.scales = {\n                x: linearAxis,\n                y: categoryAxis,\n            };\n            chartData.options.indexAxis = \"y\";\n        } else if (['pie', 'doughnut'].includes(this.el.dataset.type)) {\n            chartData.options.scales = {};\n            chartData.options.plugins.tooltip.callbacks = {\n                label: (tooltipItem) => {\n                    const label = tooltipItem.label;\n                    const secondLabel = tooltipItem.dataset.label;\n                    let final = label;\n                    if (label) {\n                        if (secondLabel) {\n                            final = label + ' - ' + secondLabel;\n                        }\n                    } else if (secondLabel) {\n                        final = secondLabel;\n                    }\n                    return final + ':' + tooltipItem.formattedValue;\n                },\n            };\n        }\n\n        // Disable animation in edit mode\n        if (this.editableMode) {\n            chartData.options.animation = {\n                duration: 0,\n            };\n        }\n\n        const canvas = this.el.querySelector('canvas');\n        window.Chart.Tooltip.positioners.custom = (elements, eventPosition) => eventPosition;\n        this.chart = new window.Chart(canvas, chartData);\n        return this._super.apply(this, arguments);\n    },\n\n    willStart: async function () {\n        await loadBundle(\"web.chartjs_lib\");\n    },\n    /**\n     * @override\n     * Discard all library changes to reset the state of the Html.\n     */\n    destroy: function () {\n        if (this.chart) { // The widget can be destroyed before start has completed\n            this.chart.destroy();\n            this.el.querySelectorAll('.chartjs-size-monitor').forEach(el => el.remove());\n        }\n        this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {string} color A css color or theme color string\n     * @returns {string} Css color\n     */\n    _convertToCssColor: function (color) {\n        if (!color) {\n            return 'transparent';\n        }\n        return weUtils.getCSSVariableValue(color, this.style) || color;\n    },\n});\n\npublicWidget.registry.chart = ChartWidget;\n\nexport default ChartWidget;\n", "/** @odoo-module **/\n\nimport {extraMenuUpdateCallbacks} from \"@website/js/content/menu\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\nconst faqHorizontal = publicWidget.Widget.extend({\n    selector: '.s_faq_horizontal',\n    disabledInEditableMode: false,\n\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n\n        this.titles = this.$el[0].getElementsByClassName('s_faq_horizontal_entry_title');\n\n        this._updateTitlesPosition();\n        this._updateTitlesPositionBound = this._updateTitlesPosition.bind(this);\n        extraMenuUpdateCallbacks.push(this._updateTitlesPositionBound);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        const indexCallback = extraMenuUpdateCallbacks.indexOf(this._updateTitlesPositionBound);\n        if (indexCallback >= 0) {\n            extraMenuUpdateCallbacks.splice(indexCallback, 1);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _updateTitlesPosition() {\n        let position = 16; // Add 1rem equivalent in px to provide a visual gap by default\n        const fixedElements = document.getElementsByClassName('o_top_fixed_element');\n\n        Array.from(fixedElements).forEach((el) => position += el.offsetHeight);\n\n        Array.from(this.titles).forEach((title) => {\n            title.style.top = `${position}px`;\n            title.style.maxHeight = `calc(100vh - ${position + 40}px)`;\n        });\n    },\n});\n\npublicWidget.registry.snippetFaqHorizontal = faqHorizontal;\n\nexport default faqHorizontal;\n", "/** @odoo-module **/\n/* global google */\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\npublicWidget.registry.GoogleMap = publicWidget.Widget.extend({\n    selector: '.s_google_map',\n    disabledInEditableMode: false,\n\n    mapColors: {\n        lightMonoMap: [{\"featureType\": \"administrative.locality\", \"elementType\": \"all\", \"stylers\": [{\"hue\": \"#2c2e33\"}, {\"saturation\": 7}, {\"lightness\": 19}, {\"visibility\": \"on\"}]}, {\"featureType\": \"landscape\", \"elementType\": \"all\", \"stylers\": [{\"hue\": \"#ffffff\"}, {\"saturation\": -100}, {\"lightness\": 100}, {\"visibility\": \"simplified\"}]}, {\"featureType\": \"poi\", \"elementType\": \"all\", \"stylers\": [{\"hue\": \"#ffffff\"}, {\"saturation\": -100}, {\"lightness\": 100}, {\"visibility\": \"off\"}]}, {\"featureType\": \"road\", \"elementType\": \"geometry\", \"stylers\": [{\"hue\": \"#bbc0c4\"}, {\"saturation\": -93}, {\"lightness\": 31}, {\"visibility\": \"simplified\"}]}, {\"featureType\": \"road\", \"elementType\": \"labels\", \"stylers\": [{\"hue\": \"#bbc0c4\"}, {\"saturation\": -93}, {\"lightness\": 31}, {\"visibility\": \"on\"}]}, {\"featureType\": \"road.arterial\", \"elementType\": \"labels\", \"stylers\": [{\"hue\": \"#bbc0c4\"}, {\"saturation\": -93}, {\"lightness\": -2}, {\"visibility\": \"simplified\"}]}, {\"featureType\": \"road.local\", \"elementType\": \"geometry\", \"stylers\": [{\"hue\": \"#e9ebed\"}, {\"saturation\": -90}, {\"lightness\": -8}, {\"visibility\": \"simplified\"}]}, {\"featureType\": \"transit\", \"elementType\": \"all\", \"stylers\": [{\"hue\": \"#e9ebed\"}, {\"saturation\": 10}, {\"lightness\": 69}, {\"visibility\": \"on\"}]}, {\"featureType\": \"water\", \"elementType\": \"all\", \"stylers\": [{\"hue\": \"#e9ebed\"}, {\"saturation\": -78}, {\"lightness\": 67}, {\"visibility\": \"simplified\"}]}],\n        lillaMap: [{elementType: \"labels\", stylers: [{saturation: -20}]}, {featureType: \"poi\", elementType: \"labels\", stylers: [{visibility: \"off\"}]}, {featureType: 'road.highway', elementType: 'labels', stylers: [{visibility: \"off\"}]}, {featureType: \"road.local\", elementType: \"labels.icon\", stylers: [{visibility: \"off\"}]}, {featureType: \"road.arterial\", elementType: \"labels.icon\", stylers: [{visibility: \"off\"}]}, {featureType: \"road\", elementType: \"geometry.stroke\", stylers: [{visibility: \"off\"}]}, {featureType: \"transit\", elementType: \"geometry.fill\", stylers: [{hue: '#2d313f'}, {visibility: \"on\"}, {lightness: 5}, {saturation: -20}]}, {featureType: \"poi\", elementType: \"geometry.fill\", stylers: [{hue: '#2d313f'}, {visibility: \"on\"}, {lightness: 5}, {saturation: -20}]}, {featureType: \"poi.government\", elementType: \"geometry.fill\", stylers: [{hue: '#2d313f'}, {visibility: \"on\"}, {lightness: 5}, {saturation: -20}]}, {featureType: \"poi.sport_complex\", elementType: \"geometry.fill\", stylers: [{hue: '#2d313f'}, {visibility: \"on\"}, {lightness: 5}, {saturation: -20}]}, {featureType: \"poi.attraction\", elementType: \"geometry.fill\", stylers: [{hue: '#2d313f'}, {visibility: \"on\"}, {lightness: 5}, {saturation: -20}]}, {featureType: \"poi.business\", elementType: \"geometry.fill\", stylers: [{hue: '#2d313f'}, {visibility: \"on\"}, {lightness: 5}, {saturation: -20}]}, {featureType: \"transit\", elementType: \"geometry.fill\", stylers: [{hue: '#2d313f'}, {visibility: \"on\"}, {lightness: 5}, {saturation: -20}]}, {featureType: \"transit.station\", elementType: \"geometry.fill\", stylers: [{hue: '#2d313f'}, {visibility: \"on\"}, {lightness: 5}, {saturation: -20}]}, {featureType: \"landscape\", stylers: [{hue: '#2d313f'}, {visibility: \"on\"}, {lightness: 5}, {saturation: -20}]}, {featureType: \"road\", elementType: \"geometry.fill\", stylers: [{hue: '#2d313f'}, {visibility: \"on\"}, {lightness: 5}, {saturation: -20}]}, {featureType: \"road.highway\", elementType: \"geometry.fill\", stylers: [{hue: '#2d313f'}, {visibility: \"on\"}, {lightness: 5}, {saturation: -20}]}, {featureType: \"water\", elementType: \"geometry\", stylers: [{hue: '#2d313f'}, {visibility: \"on\"}, {lightness: 5}, {saturation: -20}]}],\n        blueMap: [{stylers: [{hue: \"#00ffe6\"}, {saturation: -20}]}, {featureType: \"road\", elementType: \"geometry\", stylers: [{lightness: 100}, {visibility: \"simplified\"}]}, {featureType: \"road\", elementType: \"labels\", stylers: [{visibility: \"off\"}]}],\n        retroMap: [{\"featureType\": \"administrative\", \"elementType\": \"all\", \"stylers\": [{\"visibility\": \"on\"}, {\"lightness\": 33}]}, {\"featureType\": \"landscape\", \"elementType\": \"all\", \"stylers\": [{\"color\": \"#f2e5d4\"}]}, {\"featureType\": \"poi.park\", \"elementType\": \"geometry\", \"stylers\": [{\"color\": \"#c5dac6\"}]}, {\"featureType\": \"poi.park\", \"elementType\": \"labels\", \"stylers\": [{\"visibility\": \"on\"}, {\"lightness\": 20}]}, {\"featureType\": \"road\", \"elementType\": \"all\", \"stylers\": [{\"lightness\": 20}]}, {\"featureType\": \"road.highway\", \"elementType\": \"geometry\", \"stylers\": [{\"color\": \"#c5c6c6\"}]}, {\"featureType\": \"road.arterial\", \"elementType\": \"geometry\", \"stylers\": [{\"color\": \"#e4d7c6\"}]}, {\"featureType\": \"road.local\", \"elementType\": \"geometry\", \"stylers\": [{\"color\": \"#fbfaf7\"}]}, {\"featureType\": \"water\", \"elementType\": \"all\", \"stylers\": [{\"visibility\": \"on\"}, {\"color\": \"#acbcc9\"}]}],\n        flatMap: [{\"stylers\": [{\"visibility\": \"off\"}]}, {\"featureType\": \"road\", \"stylers\": [{\"visibility\": \"on\"}, {\"color\": \"#ffffff\"}]}, {\"featureType\": \"road.arterial\", \"stylers\": [{\"visibility\": \"on\"}, {\"color\": \"#fee379\"}]}, {\"featureType\": \"road.highway\", \"stylers\": [{\"visibility\": \"on\"}, {\"color\": \"#fee379\"}]}, {\"featureType\": \"landscape\", \"stylers\": [{\"visibility\": \"on\"}, {\"color\": \"#f3f4f4\"}]}, {\"featureType\": \"water\", \"stylers\": [{\"visibility\": \"on\"}, {\"color\": \"#7fc8ed\"}]}, {}, {\"featureType\": \"road\", \"elementType\": \"labels\", \"stylers\": [{\"visibility\": \"on\"}]}, {\"featureType\": \"poi.park\", \"elementType\": \"geometry.fill\", \"stylers\": [{\"visibility\": \"on\"}, {\"color\": \"#83cead\"}]}, {\"elementType\": \"labels\", \"stylers\": [{\"visibility\": \"on\"}]}, {\"featureType\": \"landscape.man_made\", \"elementType\": \"geometry\", \"stylers\": [{\"weight\": 0.9}, {\"visibility\": \"off\"}]}],\n        cobaltMap: [{\"featureType\": \"all\", \"elementType\": \"all\", \"stylers\": [{\"invert_lightness\": true}, {\"saturation\": 10}, {\"lightness\": 30}, {\"gamma\": 0.5}, {\"hue\": \"#435158\"}]}],\n        cupertinoMap: [{\"featureType\": \"water\", \"elementType\": \"geometry\", \"stylers\": [{\"color\": \"#a2daf2\"}]}, {\"featureType\": \"landscape.man_made\", \"elementType\": \"geometry\", \"stylers\": [{\"color\": \"#f7f1df\"}]}, {\"featureType\": \"landscape.natural\", \"elementType\": \"geometry\", \"stylers\": [{\"color\": \"#d0e3b4\"}]}, {\"featureType\": \"landscape.natural.terrain\", \"elementType\": \"geometry\", \"stylers\": [{\"visibility\": \"off\"}]}, {\"featureType\": \"poi.park\", \"elementType\": \"geometry\", \"stylers\": [{\"color\": \"#bde6ab\"}]}, {\"featureType\": \"poi\", \"elementType\": \"labels\", \"stylers\": [{\"visibility\": \"off\"}]}, {\"featureType\": \"poi.medical\", \"elementType\": \"geometry\", \"stylers\": [{\"color\": \"#fbd3da\"}]}, {\"featureType\": \"poi.business\", \"stylers\": [{\"visibility\": \"off\"}]}, {\"featureType\": \"road\", \"elementType\": \"geometry.stroke\", \"stylers\": [{\"visibility\": \"off\"}]}, {\"featureType\": \"road\", \"elementType\": \"labels\", \"stylers\": [{\"visibility\": \"off\"}]}, {\"featureType\": \"road.highway\", \"elementType\": \"geometry.fill\", \"stylers\": [{\"color\": \"#ffe15f\"}]}, {\"featureType\": \"road.highway\", \"elementType\": \"geometry.stroke\", \"stylers\": [{\"color\": \"#efd151\"}]}, {\"featureType\": \"road.arterial\", \"elementType\": \"geometry.fill\", \"stylers\": [{\"color\": \"#ffffff\"}]}, {\"featureType\": \"road.local\", \"elementType\": \"geometry.fill\", \"stylers\": [{\"color\": \"black\"}]}, {\"featureType\": \"transit.station.airport\", \"elementType\": \"geometry.fill\", \"stylers\": [{\"color\": \"#cfb2db\"}]}],\n        carMap: [{\"featureType\": \"administrative\", \"stylers\": [{\"visibility\": \"off\"}]}, {\"featureType\": \"poi\", \"stylers\": [{\"visibility\": \"simplified\"}]}, {\"featureType\": \"road\", \"stylers\": [{\"visibility\": \"simplified\"}]}, {\"featureType\": \"water\", \"stylers\": [{\"visibility\": \"simplified\"}]}, {\"featureType\": \"transit\", \"stylers\": [{\"visibility\": \"simplified\"}]}, {\"featureType\": \"landscape\", \"stylers\": [{\"visibility\": \"simplified\"}]}, {\"featureType\": \"road.highway\", \"stylers\": [{\"visibility\": \"off\"}]}, {\"featureType\": \"road.local\", \"stylers\": [{\"visibility\": \"on\"}]}, {\"featureType\": \"road.highway\", \"elementType\": \"geometry\", \"stylers\": [{\"visibility\": \"on\"}]}, {\"featureType\": \"water\", \"stylers\": [{\"color\": \"#84afa3\"}, {\"lightness\": 52}]}, {\"stylers\": [{\"saturation\": -77}]}, {\"featureType\": \"road\"}],\n        bwMap: [{stylers: [{hue: \"#00ffe6\"}, {saturation: -100}]}, {featureType: \"road\", elementType: \"geometry\", stylers: [{lightness: 100}, {visibility: \"simplified\"}]}, {featureType: \"road\", elementType: \"labels\", stylers: [{visibility: \"off\"}]}],\n    },\n\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n\n        if (typeof google !== 'object' || typeof google.maps !== 'object') {\n            await new Promise(resolve => {\n                this.trigger_up('gmap_api_request', {\n                    editableMode: this.editableMode,\n                    onSuccess: () => resolve(),\n                });\n            });\n            // The animation will be restarted for all maps as soon as the\n            // google map script has been executed.\n            return;\n        }\n\n        // Define a default map's colors set\n        const std = [];\n        new google.maps.StyledMapType(std, {name: \"Std Map\"});\n\n        // Default options, will be overwritten by the user\n        const myOptions = {\n            zoom: 12,\n            center: new google.maps.LatLng(50.854975, 4.3753899),\n            mapTypeId: google.maps.MapTypeId.ROADMAP,\n            panControl: false,\n            zoomControl: false,\n            mapTypeControl: false,\n            streetViewControl: false,\n            scrollwheel: false,\n            mapTypeControlOptions: {\n                mapTypeIds: [google.maps.MapTypeId.ROADMAP, 'map_style']\n            }\n        };\n\n        // Render Map\n        const mapC = this.$('.map_container');\n        const map = new google.maps.Map(mapC.get(0), myOptions);\n\n        // Update GPS position\n        const p = this.el.dataset.mapGps.substring(1).slice(0, -1).split(',');\n        const gps = new google.maps.LatLng(p[0], p[1]);\n        map.setCenter(gps);\n\n        // Update Map on screen resize\n        window.addEventListener('resize', () => {\n            map.setCenter(gps);\n        });\n\n        // Create Marker & Infowindow\n        const markerOptions = {\n            map: map,\n            animation: google.maps.Animation.DROP,\n            position: new google.maps.LatLng(p[0], p[1])\n        };\n        if (this.el.dataset.pinStyle === 'flat') {\n            markerOptions.icon = '/website/static/src/img/snippets_thumbs/s_google_map_marker.png';\n        }\n        new google.maps.Marker(markerOptions);\n\n        map.setMapTypeId(google.maps.MapTypeId[this.el.dataset.mapType]); // Update Map Type\n        map.setZoom(parseInt(this.el.dataset.mapZoom)); // Update Map Zoom\n\n        // Update Map Color\n        const mapColorAttr = this.el.dataset.mapColor;\n        if (mapColorAttr) {\n            const mapColor = this.mapColors[mapColorAttr];\n            map.mapTypes.set('map_style', new google.maps.StyledMapType(mapColor, {name: \"Styled Map\"}));\n            map.setMapTypeId('map_style');\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport {generateGMapLink, generateGMapIframe} from '@website/js/utils';\nimport { ObservingCookieWidgetMixin } from \"@website/snippets/observing_cookie_mixin\";\n\npublicWidget.registry.Map = publicWidget.Widget.extend(ObservingCookieWidgetMixin, {\n    selector: '.s_map',\n\n    /**\n     * @override\n     */\n    start() {\n        if (!this.el.querySelector('.s_map_embedded')) {\n            // The iframe is not found inside the snippet. This is probably due\n            // to the sanitization of a field during the save, like in a product\n            // description field.\n            // In such cases, reconstruct the iframe.\n            const dataset = this.el.dataset;\n            if (dataset.mapAddress) {\n                const iframeEl = generateGMapIframe();\n                this.el.querySelector('.s_map_color_filter').before(iframeEl);\n                this._manageIframeSrc(this.el, generateGMapLink(dataset));\n            }\n        }\n        return this._super(...arguments);\n    },\n});\n\nexport default publicWidget.registry.Map;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { renderToString } from \"@web/core/utils/render\";\nimport { listenSizeChange, utils as uiUtils } from \"@web/core/ui/ui_service\";\n\nimport { markup } from \"@odoo/owl\";\n\nconst DEFAULT_NUMBER_OF_ELEMENTS = 4;\nconst DEFAULT_NUMBER_OF_ELEMENTS_SM = 1;\n\nconst DynamicSnippet = publicWidget.Widget.extend({\n    selector: '.s_dynamic_snippet',\n    read_events: {\n        'click [data-url]': '_onCallToAction',\n    },\n    disabledInEditableMode: false,\n\n    /**\n     *\n     * @override\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n        /**\n         * The dynamic filter data source data formatted with the chosen template.\n         * Can be accessed when overriding the _render_content() function in order to generate\n         * a new renderedContent from the original data.\n         *\n         * @type {*|jQuery.fn.init|jQuery|HTMLElement}\n         */\n        this.data = [];\n        this.renderedContent = '';\n        this.isDesplayedAsMobile = uiUtils.isSmall();\n        this.unique_id = uniqueId(\"s_dynamic_snippet_\");\n        this.template_key = 'website.s_dynamic_snippet.grid';\n    },\n    /**\n     *\n     * @override\n     */\n    willStart: function () {\n        return this._super.apply(this, arguments).then(\n            () => Promise.all([\n                this._fetchData(),\n            ])\n        );\n    },\n    /**\n     *\n     * @override\n     */\n    start: function () {\n        return this._super.apply(this, arguments)\n            .then(() => {\n                this._setupSizeChangedManagement(true);\n                this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerUnactive();\n                this._render();\n                this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerActive();\n            });\n    },\n    /**\n     *\n     * @override\n     */\n    destroy: function () {\n        this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerUnactive();\n        this._toggleVisibility(false);\n        this._setupSizeChangedManagement(false);\n        this._clearContent();\n        this.options.wysiwyg && this.options.wysiwyg.odooEditor.observerActive();\n        this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _clearContent: function () {\n        const $templateArea = this.$el.find('.dynamic_snippet_template');\n        this.trigger_up('widgets_stop_request', {\n            $target: $templateArea,\n        });\n        $templateArea.html('');\n    },\n    /**\n     * Method to be overridden in child components if additional configuration elements\n     * are required in order to fetch data.\n     * @private\n     */\n    _isConfigComplete: function () {\n        return this.$el.get(0).dataset.filterId !== undefined && this.$el.get(0).dataset.templateKey !== undefined;\n    },\n    /**\n     * Method to be overridden in child components in order to provide a search\n     * domain if needed.\n     * @private\n     */\n    _getSearchDomain: function () {\n        return [];\n    },\n    /**\n     * Method to be overridden in child components in order to add custom parameters if needed.\n     * @private\n     */\n    _getRpcParameters: function () {\n        return {};\n    },\n    /**\n     * Fetches the data.\n     * @private\n     */\n    async _fetchData() {\n        if (this._isConfigComplete()) {\n            const nodeData = this.el.dataset;\n            const filterFragments = await rpc(\n                '/website/snippet/filters',\n                Object.assign({\n                        'filter_id': parseInt(nodeData.filterId),\n                        'template_key': nodeData.templateKey,\n                        'limit': parseInt(nodeData.numberOfRecords),\n                        'search_domain': this._getSearchDomain(),\n                        'with_sample': this.editableMode,\n                    },\n                    this._getRpcParameters(),\n                    JSON.parse(this.el.dataset?.customTemplateData || \"{}\")\n                )\n            );\n            this.data = filterFragments.map(markup);\n        } else {\n            this.data = [];\n        }\n    },\n    /**\n     * Method to be overridden in child components in order to prepare content\n     * before rendering.\n     * @private\n     */\n    _prepareContent: function () {\n        this.renderedContent = renderToString(\n            this.template_key,\n            this._getQWebRenderOptions()\n        );\n    },\n    /**\n     * Method to be overridden in child components in order to prepare QWeb\n     * options.\n     * @private\n     */\n     _getQWebRenderOptions: function () {\n        const dataset = this.el.dataset;\n        const numberOfRecords = parseInt(dataset.numberOfRecords);\n        let numberOfElements;\n        if (uiUtils.isSmall()) {\n            numberOfElements = parseInt(dataset.numberOfElementsSmallDevices) || DEFAULT_NUMBER_OF_ELEMENTS_SM;\n        } else {\n            numberOfElements = parseInt(dataset.numberOfElements) || DEFAULT_NUMBER_OF_ELEMENTS;\n        }\n        const chunkSize = numberOfRecords < numberOfElements ? numberOfRecords : numberOfElements;\n        return {\n            chunkSize: chunkSize,\n            data: this.data,\n            unique_id: this.unique_id,\n            extraClasses: dataset.extraClasses || '',\n            columnClasses: dataset.columnClasses || '',\n        };\n    },\n    /**\n     *\n     * @private\n     */\n    _render: function () {\n        if (this.data.length > 0 || this.editableMode) {\n            this.$el.removeClass('o_dynamic_snippet_empty');\n            this._prepareContent();\n        } else {\n            this.$el.addClass('o_dynamic_snippet_empty');\n            this.renderedContent = '';\n        }\n        this._renderContent();\n        this.trigger_up('widgets_start_request', {\n            $target: this.$el.children(),\n            options: {parent: this},\n            editableMode: this.editableMode,\n        });\n    },\n    /**\n     * @private\n     */\n    _renderContent: function () {\n        const $templateArea = this.$el.find('.dynamic_snippet_template');\n        this.trigger_up('widgets_stop_request', {\n            $target: $templateArea,\n        });\n        const mainPageUrl = this._getMainPageUrl();\n        const allContentLink = this.el.querySelector(\".s_dynamic_snippet_main_page_url\");\n        if (allContentLink && mainPageUrl) {\n            allContentLink.href = mainPageUrl;\n            allContentLink.classList.remove(\"d-none\");\n        }\n        $templateArea.html(this.renderedContent);\n        // TODO this is probably not the only public widget which creates DOM\n        // which should be attached to another public widget. Maybe a generic\n        // method could be added to properly do this operation of DOM addition.\n        this.trigger_up('widgets_start_request', {\n            $target: $templateArea,\n            editableMode: this.editableMode,\n        });\n        // Same as above and probably should be done automatically for any\n        // bootstrap behavior (apparently needed since BS 5.3): start potential\n        // carousel in new content (according to their data-bs-ride and other\n        // dataset attributes). Note: done here and not in dynamic carousel\n        // extension, because: why not?\n        // (TODO review + See interaction with \"slider\" public widget).\n        setTimeout(() => {\n            $templateArea[0].querySelectorAll('.carousel').forEach(carouselEl => {\n                if (carouselEl.dataset.bsInterval === \"0\") {\n                    delete carouselEl.dataset.bsRide;\n                    delete carouselEl.dataset.bsInterval;\n                }\n                window.Carousel.getInstance(carouselEl)?.dispose();\n                if (!this.editableMode) {\n                    window.Carousel.getOrCreateInstance(carouselEl);\n                }\n            });\n        }, 0);\n    },\n    /**\n     *\n     * @param {Boolean} enable\n     * @private\n     */\n    _setupSizeChangedManagement: function (enable) {\n        if (enable === true) {\n            this.removeSizeListener = listenSizeChange(this._onSizeChanged.bind(this));\n        } else if (this.removeSizeListener) {\n            this.removeSizeListener();\n            delete this.removeSizeListener;\n        }\n    },\n    /**\n     *\n     * @param visible\n     * @private\n     */\n    _toggleVisibility: function (visible) {\n        this.$el.toggleClass('o_dynamic_snippet_empty', !visible);\n    },\n    /**\n     * Returns the main URL of the module related to the active filter.\n     *\n     * @private\n     */\n    _getMainPageUrl() {\n        return '';\n    },\n\n    //------------------------------------- -------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Navigates to the call to action url.\n     * @private\n     */\n    _onCallToAction: function (ev) {\n        window.location = $(ev.currentTarget).attr('data-url');\n    },\n    /**\n     * Called when the size has reached a new bootstrap breakpoint.\n     *\n     * @private\n     */\n    _onSizeChanged: function () {\n        if (this.isDesplayedAsMobile !== uiUtils.isSmall()) {\n            this.isDesplayedAsMobile = uiUtils.isSmall();\n            this._render();\n        }\n    },\n});\n\npublicWidget.registry.dynamic_snippet = DynamicSnippet;\n\nexport default DynamicSnippet;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport DynamicSnippet from \"@website/snippets/s_dynamic_snippet/000\";\nimport { utils as uiUtils } from \"@web/core/ui/ui_service\";\n\nconst DynamicSnippetCarousel = DynamicSnippet.extend({\n    selector: '.s_dynamic_snippet_carousel',\n    /**\n     * @override\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n        this.template_key = 'website.s_dynamic_snippet.carousel';\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _getQWebRenderOptions: function () {\n        return Object.assign(\n            this._super.apply(this, arguments),\n            {\n                interval: parseInt(this.el.dataset.carouselInterval),\n                rowPerSlide: parseInt(uiUtils.isSmall() ? 1 : this.el.dataset.rowPerSlide || 1),\n                arrowPosition: this.el.dataset.arrowPosition || '',\n            },\n        );\n    },\n});\npublicWidget.registry.dynamic_snippet_carousel = DynamicSnippetCarousel;\n\nexport default DynamicSnippetCarousel;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { cloneContentEls } from \"@website/js/utils\";\n\nconst EmbedCodeWidget = publicWidget.Widget.extend({\n    selector: \".s_embed_code\",\n    disabledInEditableMode: false,\n\n    /**\n     * @override\n     */\n    async start() {\n        this.embedCodeEl = this.el.querySelector(\".s_embed_code_embedded\");\n\n        if (this.editableMode && this.embedCodeEl.offsetHeight === 0) {\n            // Shows a placeholder message in edit mode to be able to select\n            // the snippet if it's visually empty.\n            const placeholderEl = document.createElement(\"div\");\n            placeholderEl.classList\n                .add(\"s_embed_code_placeholder\", \"alert\", \"alert-info\", \"pt16\", \"pb16\");\n            placeholderEl.textContent = _t(\"Your Embed Code snippet doesn't have anything to display. Click on Edit to modify it.\");\n            this.el.querySelector(\".s_embed_code_embedded\").appendChild(placeholderEl);\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n\n        // Just before entering edit mode, reinitialize the snippet's content,\n        // without <script> elements. This is both done so that scripts don't\n        // affect the DOM in edit mode, and to remove elements that would have\n        // been introduced by a script.\n        if (!this.editableMode) {\n            const templateContent = this.el.querySelector(\"template.s_embed_code_saved\").content;\n            this.embedCodeEl.replaceChildren(cloneContentEls(templateContent));\n        }\n    },\n});\n\npublicWidget.registry.EmbedCode = EmbedCodeWidget;\n\nexport default EmbedCodeWidget;\n", "/** @odoo-module **/\n\n    import {ReCaptcha} from \"@google_recaptcha/js/recaptcha\";\n    import { session } from \"@web/session\";\n    import { user } from \"@web/core/user\";\n    import publicWidget from \"@web/legacy/js/public/public_widget\";\n    import { delay } from \"@web/core/utils/concurrency\";\n    import { debounce } from \"@web/core/utils/timing\";\n    import { _t } from \"@web/core/l10n/translation\";\n    import { renderToElement } from \"@web/core/utils/render\";\n    import { post } from \"@web/core/network/http_service\";\n    import { localization } from \"@web/core/l10n/localization\";\nimport {\n    formatDate,\n    formatDateTime,\n    parseDate,\n    parseDateTime,\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { addLoadingEffect } from \"@web/core/utils/ui\";\nimport { scrollTo } from \"@web_editor/js/common/scrolling\";\nconst DEBOUNCE = 400;\nconst { DateTime } = luxon;\nimport wUtils from '@website/js/utils';\n\n    publicWidget.registry.EditModeWebsiteForm = publicWidget.Widget.extend({\n        selector: '.s_website_form form, form.s_website_form', // !compatibility\n        disabledInEditableMode: false,\n        /**\n         * @override\n         */\n        start: function () {\n            if (this.editableMode) {\n                // We do not initialize the datetime picker in edit mode but want the dates to be formated\n                this.el.querySelectorAll('.s_website_form_input.datetimepicker-input').forEach(el => {\n                    const value = el.getAttribute('value');\n                    if (value) {\n                    const format =\n                        el.closest(\".s_website_form_field\").dataset.type === \"date\"\n                            ? formatDate\n                            : formatDateTime;\n                        el.value = format(DateTime.fromSeconds(parseInt(value)));\n                    }\n                });\n            }\n            return this._super(...arguments);\n        },\n    });\n\n    publicWidget.registry.s_website_form = publicWidget.Widget.extend({\n        selector: '.s_website_form form, form.s_website_form', // !compatibility\n        events: {\n            'click .s_website_form_send, .o_website_form_send': 'send', // !compatibility\n            'submit': 'send',\n            \"change input[type=file]\": \"_onFileChange\",\n            \"click input.o_add_files_button\": \"_onAddFilesButtonClick\",\n            \"click .o_file_delete\": \"_onFileDeleteClick\",\n        },\n\n        /**\n         * @constructor\n         */\n        init: function () {\n            this._super(...arguments);\n            this._recaptcha = new ReCaptcha();\n            this.initialValues = new Map();\n            this._visibilityFunctionByFieldName = new Map();\n            this._visibilityFunctionByFieldEl = new Map();\n            this.__started = new Promise(resolve => this.__startResolve = resolve);\n            this.orm = this.bindService(\"orm\");\n        },\n        willStart: async function () {\n            const res = this._super(...arguments);\n            if (!this.el.classList.contains('s_website_form_no_recaptcha')) {\n                this._recaptchaLoaded = true;\n                this._recaptcha.loadLibs();\n            }\n            // fetch user data (required by fill-with behavior)\n            this.preFillValues = {};\n            if (user.userId) {\n                this.preFillValues = (await this.orm.read(\n                    \"res.users\",\n                    [user.userId],\n                    this._getUserPreFillFields()\n                ))[0] || {};\n            }\n            return res;\n        },\n        start: function () {\n            // Reset the form first, as it is still filled when coming back\n            // after a redirect.\n            this.resetForm();\n\n            // Prepare visibility data and update field visibilities\n            const visibilityFunctionsByFieldName = new Map();\n            for (const fieldEl of this.el.querySelectorAll('[data-visibility-dependency]')) {\n                const inputName = fieldEl.querySelector('.s_website_form_input').name;\n                if (!visibilityFunctionsByFieldName.has(inputName)) {\n                    visibilityFunctionsByFieldName.set(inputName, []);\n                }\n                const func = this._buildVisibilityFunction(fieldEl);\n                visibilityFunctionsByFieldName.get(inputName).push(func);\n                this._visibilityFunctionByFieldEl.set(fieldEl, func);\n            }\n            for (const [name, funcs] of visibilityFunctionsByFieldName.entries()) {\n                this._visibilityFunctionByFieldName.set(name, () => funcs.some(func => func()));\n            }\n\n            this._onFieldInputDebounced = debounce(this._onFieldInput.bind(this), 400);\n            this.$el.on('input.s_website_form', '.s_website_form_field', this._onFieldInputDebounced);\n\n            this.$allDates = this.$el.find('.s_website_form_datetime, .o_website_form_datetime, .s_website_form_date, .o_website_form_date');\n            this.disableDateTimePickers = [];\n            if (!this.editableMode) {\n                for (const field of this.$allDates) {\n                    const input = field.querySelector(\"input\");\n                    const defaultValue = input.getAttribute(\"value\");\n                    this.disableDateTimePickers.push(this.call(\"datetime_picker\", \"create\", {\n                        target: input,\n                        onChange: () => input.dispatchEvent(new Event(\"input\", { bubbles: true })),\n                        pickerProps: {\n                            type: field.matches('.s_website_form_date, .o_website_form_date') ? 'date' : 'datetime',\n                            value: defaultValue && DateTime.fromSeconds(parseInt(defaultValue)),\n                        },\n                    }).enable());\n                }\n                this.$allDates.addClass('s_website_form_datepicker_initialized');\n            }\n\n            // Display form values from tag having data-for attribute\n            // It's necessary to handle field values generated on server-side\n            // Because, using t-att- inside form make it non-editable\n            // Data-fill-with attribute is given during registry and is used by\n            // to know which user data should be used to prfill fields.\n            let dataForValues = wUtils.getParsedDataFor(this.el.id, document);\n            this.editTranslations = !!this._getContext(true).edit_translations;\n            // On the \"edit_translations\" mode, a <span/> with a translated term\n            // will replace the attribute value, leading to some inconsistencies\n            // (setting again the <span> on the attributes after the editor's\n            // cleanup, setting wrong values on the attributes after translating\n            // default values...)\n            if (!this.editTranslations\n                    && (dataForValues || Object.keys(this.preFillValues).length)) {\n                dataForValues = dataForValues || {};\n                const fieldNames = this.$target.serializeArray().map(el => el.name);\n                // All types of inputs do not have a value property (eg:hidden),\n                // for these inputs any function that is supposed to put a value\n                // property actually puts a HTML value attribute. Because of\n                // this, we have to clean up these values at destroy or else the\n                // data loaded here could become default values. We could set\n                // the values to submit() for these fields but this could break\n                // customizations that use the current behavior as a feature.\n                for (const name of fieldNames) {\n                    const fieldEl = this.el.querySelector(`[name=\"${CSS.escape(name)}\"]`);\n\n                    // In general, we want the data-for and prefill values to\n                    // take priority over set default values. The 'email_to'\n                    // field is however treated as an exception at the moment\n                    // so that values set by users are always used.\n                    if (name === 'email_to' && fieldEl.value\n                            // The following value is the default value that\n                            // is set if the form is edited in any way. (see the\n                            // @website/js/form_editor_registry module in editor\n                            // assets bundle).\n                            // TODO that value should probably never be forced\n                            // unless explicitely manipulated by the user or on\n                            // custom form addition but that seems risky to\n                            // change as a stable fix.\n                            && fieldEl.value !== 'info@yourcompany.example.com') {\n                        continue;\n                    }\n\n                    let newValue;\n                    if (dataForValues && dataForValues[name]) {\n                        newValue = dataForValues[name];\n                    } else if (this.preFillValues[fieldEl.dataset.fillWith]) {\n                        newValue = this.preFillValues[fieldEl.dataset.fillWith];\n                    }\n                    if (newValue) {\n                        this.initialValues.set(fieldEl, fieldEl.getAttribute('value'));\n                        fieldEl.value = newValue;\n                    }\n                }\n            }\n            this._updateFieldsVisibility();\n\n            if (session.geoip_phone_code) {\n                this.el.querySelectorAll('input[type=\"tel\"]').forEach(telField => {\n                    if (!telField.value) {\n                        telField.value = '+' + session.geoip_phone_code;\n                    }\n                });\n            }\n            // Check disabled states\n            this.inputEls = this.el.querySelectorAll('.s_website_form_field.s_website_form_field_hidden_if .s_website_form_input');\n            this._disabledStates = new Map();\n            for (const inputEl of this.inputEls) {\n                this._disabledStates[inputEl] = inputEl.disabled;\n            }\n\n            // Add the files zones where the file blocks will be displayed.\n            this.el.querySelectorAll(\"input[type=file]\").forEach(inputEl => {\n                const filesZoneEl = document.createElement(\"DIV\");\n                filesZoneEl.classList.add(\"o_files_zone\", \"row\", \"gx-1\");\n                inputEl.parentNode.insertBefore(filesZoneEl, inputEl);\n            });\n\n            return this._super(...arguments).then(() => this.__startResolve());\n        },\n\n        /**\n         * @override\n         */\n        destroy: function () {\n            this._super.apply(this, arguments);\n            this.$el.find('button').off('click');\n\n            // Empty inputs\n            this.resetForm();\n\n            // Apply default values\n            this.el.querySelectorAll('input[type=\"text\"], input[type=\"email\"], input[type=\"number\"]').forEach(el => {\n                let value = el.getAttribute('value');\n                if (value) {\n                    if (el.classList.contains('datetimepicker-input')) {\n                        const format =\n                            el.closest(\".s_website_form_field\").dataset.type === \"date\"\n                                ? formatDate\n                                : formatDateTime;\n                        value = format(DateTime.fromSeconds(parseInt(value)));\n                    }\n                    el.value = value;\n                }\n            });\n            this.el.querySelectorAll('textarea').forEach(el => el.value = el.textContent);\n\n            // Remove saving of the error colors\n            this.$el.find('.o_has_error').removeClass('o_has_error').find('.form-control, .form-select').removeClass('is-invalid');\n\n            // Remove the status message\n            this.$el.find('#s_website_form_result, #o_website_form_result').empty(); // !compatibility\n\n            // Remove the success message and display the form\n            this.$el.removeClass('d-none');\n            this.$el.parent().find('.s_website_form_end_message').addClass('d-none');\n\n            // Reinitialize dates\n            this.$allDates.removeClass('s_website_form_datepicker_initialized');\n\n            // Restore disabled attribute\n            for (const inputEl of this.inputEls) {\n                inputEl.disabled = !!this._disabledStates.get(inputEl);\n            }\n\n            // All 'hidden if' fields start with d-none\n            this.el.querySelectorAll('.s_website_form_field_hidden_if:not(.d-none)').forEach(el => el.classList.add('d-none'));\n\n            // Reset the initial default values.\n            for (const [fieldEl, initialValue] of this.initialValues.entries()) {\n                if (initialValue) {\n                    fieldEl.setAttribute('value', initialValue);\n                } else {\n                    fieldEl.removeAttribute('value');\n                }\n            }\n\n            this.$el.off('.s_website_form');\n            for (const disableDateTimePicker of this.disableDateTimePickers) {\n                disableDateTimePicker();\n            }\n        },\n\n        send: async function (e) {\n            e.preventDefault(); // Prevent the default submit behavior\n             // Prevent users from crazy clicking\n            const $button = this.$el.find('.s_website_form_send, .o_website_form_send');\n            $button.addClass('disabled') // !compatibility\n                   .attr('disabled', 'disabled');\n            this.restoreBtnLoading = addLoadingEffect($button[0]);\n\n            var self = this;\n\n            self.$el.find('#s_website_form_result, #o_website_form_result').empty(); // !compatibility\n            if (!self.check_error_fields({})) {\n                if (this.fileInputError) {\n                    const errorMessage = this.fileInputError.type === \"number\"\n                        ? _t(\n                            \"Please fill in the form correctly. You uploaded too many files. (Maximum %s files)\", \n                            this.fileInputError.limit\n                        )\n                        : _t(\n                            \"Please fill in the form correctly. The file \u201c%(file name)s\u201d is too large. (Maximum %(max)s MB)\", \n                            { \"file name\": this.fileInputError.fileName, max:this.fileInputError.limit }\n                        );\n                    this.update_status(\"error\", errorMessage);\n                    delete this.fileInputError;\n                } else {\n                    this.update_status(\"error\", _t(\"Please fill in the form correctly.\"));\n                }\n                return false;\n            }\n\n            // Prepare form inputs\n            this.form_fields = this.$el.serializeArray();\n            $.each(this.$el.find('input[type=file]:not([disabled])'), (outer_index, input) => {\n                $.each($(input).prop('files'), function (index, file) {\n                    // Index field name as ajax won't accept arrays of files\n                    // when aggregating multiple files into a single field value\n                    self.form_fields.push({\n                        name: input.name + '[' + outer_index + '][' + index + ']',\n                        value: file\n                    });\n                });\n            });\n\n            // Serialize form inputs into a single object\n            // Aggregate multiple values into arrays\n            var form_values = {};\n            this.form_fields.forEach((input) => {\n                if (input.name in form_values) {\n                    // If a value already exists for this field,\n                    // we are facing a x2many field, so we store\n                    // the values in an array.\n                    if (Array.isArray(form_values[input.name])) {\n                        form_values[input.name].push(input.value);\n                    } else {\n                        form_values[input.name] = [form_values[input.name], input.value];\n                    }\n                } else {\n                    if (input.value !== '') {\n                        form_values[input.name] = input.value;\n                    }\n                }\n            });\n\n            // force server date format usage for existing fields\n            this.$el.find('.s_website_form_field:not(.s_website_form_custom)')\n            .find('.s_website_form_date, .s_website_form_datetime').each(function () {\n                const inputEl = this.querySelector('input');\n                const { value } = inputEl;\n                if (!value) {\n                    return;\n                }\n\n                form_values[inputEl.getAttribute(\"name\")] = this.matches(\".s_website_form_date\")\n                    ? serializeDate(parseDate(value))\n                    : serializeDateTime(parseDateTime(value));\n            });\n\n            if (this._recaptchaLoaded) {\n                const tokenObj = await this._recaptcha.getToken('website_form');\n                if (tokenObj.token) {\n                    form_values['recaptcha_token_response'] = tokenObj.token;\n                } else if (tokenObj.error) {\n                    self.update_status('error', tokenObj.error);\n                    return false;\n                }\n            }\n\n            if (odoo.csrf_token) {\n                form_values.csrf_token = odoo.csrf_token;\n            }\n\n            const formData = new FormData();\n            for (const [key, value] of Object.entries(form_values)) {\n                formData.append(key, value);\n            }\n\n            // Post form and handle result\n            post(this.$el.attr('action') + (this.$el.data('force_action') || this.$el.data('model_name')), formData)\n            .then(async function (result_data) {\n                // Restore send button behavior\n                self.$el.find('.s_website_form_send, .o_website_form_send')\n                    .removeAttr('disabled')\n                    .removeClass('disabled'); // !compatibility\n                if (!result_data.id) {\n                    // Failure, the server didn't return the created record ID\n                    self.update_status('error', result_data.error ? result_data.error : false);\n                    if (result_data.error_fields) {\n                        // If the server return a list of bad fields, show these fields for users\n                        self.check_error_fields(result_data.error_fields);\n                    }\n                } else {\n                    // Success, redirect or update status\n                    let successMode = self.el.dataset.successMode;\n                    let successPage = self.el.dataset.successPage;\n                    if (!successMode) {\n                        successPage = self.$el.attr('data-success_page'); // Compatibility\n                        successMode = successPage ? 'redirect' : 'nothing';\n                    }\n                    switch (successMode) {\n                        case 'redirect': {\n                            let hashIndex = successPage.indexOf(\"#\");\n                            if (hashIndex > 0) {\n                                // URL containing an anchor detected: extract\n                                // the anchor from the URL if the URL is the\n                                // same as the current page URL so we can scroll\n                                // directly to the element (if found) later\n                                // instead of redirecting.\n                                // Note that both currentUrlPath and successPage\n                                // can exist with or without a trailing slash\n                                // before the hash (e.g. \"domain.com#footer\" or\n                                // \"domain.com/#footer\"). Therefore, if they are\n                                // not present, we add them to be able to\n                                // compare the two variables correctly.\n                                let currentUrlPath = window.location.pathname;\n                                if (!currentUrlPath.endsWith(\"/\")) {\n                                    currentUrlPath = currentUrlPath + \"/\";\n                                }\n                                if (!successPage.includes(\"/#\")) {\n                                    successPage = successPage.replace(\"#\", \"/#\");\n                                    hashIndex++;\n                                }\n                                if ([successPage, \"/\" + session.lang_url_code + successPage].some(link => link.startsWith(currentUrlPath + '#'))) {\n                                    successPage = successPage.substring(hashIndex);\n                                }\n                            }\n                            if (successPage.charAt(0) === \"#\") {\n                                const successAnchorEl = document.getElementById(successPage.substring(1));\n                                if (successAnchorEl) {\n                                    // Check if the target of the link is a modal.\n                                    if (successAnchorEl.classList.contains(\"modal\")) {\n                                        // Trigger a \"hashChange\" event to\n                                        // notify the popup widget to show the\n                                        // popup.\n                                        window.location.href = successPage;\n                                    } else {\n                                        await scrollTo(successAnchorEl, {\n                                            duration: 500,\n                                            extraOffset: 0,\n                                        });\n                                    }\n                                }\n                                break;\n                            }\n                            $(window.location).attr('href', successPage);\n                            return;\n                        }\n                        case 'message': {\n                            // Prevent double-clicking on the send button and\n                            // add a upload loading effect (delay before success\n                            // message)\n                            await delay(DEBOUNCE);\n\n                            self.el.classList.add('d-none');\n                            self.el.parentElement.querySelector('.s_website_form_end_message').classList.remove('d-none');\n                            break;\n                        }\n                        default: {\n                            // Prevent double-clicking on the send button and\n                            // add a upload loading effect (delay before success\n                            // message)\n                            await delay(DEBOUNCE);\n\n                            self.update_status('success');\n                            break;\n                        }\n                    }\n\n                    self.resetForm();\n                    self.restoreBtnLoading();\n                }\n            })\n            .catch(error => {\n                this.update_status(\n                    'error',\n                    error.status && error.status === 413 ? _t(\"Uploaded file is too large.\") : \"\",\n                );\n            });\n        },\n\n        /**\n         * Resets a form.\n         */\n        resetForm() {\n            this.el.reset();\n\n            // For file inputs, remove the files zone, restore the file input\n            // and remove the files list.\n            this.el.querySelectorAll(\"input[type=file]\").forEach(inputEl => {\n                const fieldEl = inputEl.closest(\".s_website_form_field\");\n                fieldEl.querySelectorAll(\".o_files_zone\").forEach(el => el.remove());\n                fieldEl.querySelectorAll(\".o_add_files_button\").forEach(el => el.remove());\n                inputEl.classList.remove(\"d-none\");\n                delete inputEl.fileList;\n            });\n        },\n\n        check_error_fields: function (error_fields) {\n            var self = this;\n            var form_valid = true;\n            // Loop on all fields\n            this.$el.find('.form-field, .s_website_form_field').each(function (k, field) { // !compatibility\n                var $field = $(field);\n                // FIXME that seems broken, \"for\" does not contain the field\n                // but this is used to retrieve errors sent from the server...\n                // need more investigation.\n                var field_name = $field.find('.col-form-label').attr('for');\n\n                // Validate inputs for this field\n                var inputs = $field.find('.s_website_form_input, .o_website_form_input').not('#editable_select'); // !compatibility\n                var invalid_inputs = inputs.toArray().filter(function (input, k, inputs) {\n                    // Special check for multiple required checkbox for same\n                    // field as it seems checkValidity forces every required\n                    // checkbox to be checked, instead of looking at other\n                    // checkboxes with the same name and only requiring one\n                    // of them to be valid.\n                    if (input.required && input.type === 'checkbox') {\n                        // Considering we are currently processing a single\n                        // field, we can assume that all checkboxes in the\n                        // inputs variable have the same name\n                        // TODO should be improved: probably do not need to\n                        // filter neither on required, nor on checkbox and\n                        // checking the validity of the group of checkbox is\n                        // currently done for each checkbox of that group...\n                        var checkboxes = inputs.filter(input => input.required && input.type === 'checkbox');\n                        return !checkboxes.some((checkbox) => checkbox.checkValidity());\n\n                    // Special cases for dates and datetimes\n                    // FIXME this seems like dead code, the inputs do not use\n                    // those classes, their parent does (but it seemed to work\n                    // at some point given that https://github.com/odoo/odoo/commit/75e03c0f7692a112e1b0fa33267f4939363f3871\n                    // was made)... need more investigation (if restored,\n                    // consider checking the date inputs are not disabled before\n                    // saying they are invalid (see checkValidity used here))\n                    } else if ($(input).hasClass('s_website_form_date') || $(input).hasClass('o_website_form_date')) { // !compatibility\n                        const date = parseDate(input.value);\n                        if (!date || !date.isValid) {\n                            return true;\n                        }\n                    } else if ($(input).hasClass('s_website_form_datetime') || $(input).hasClass('o_website_form_datetime')) { // !compatibility\n                        const date = parseDateTime(input.value);\n                        if (!date || !date.isValid) {\n                            return true;\n                        }\n                    } else if (input.type === \"file\" && !self.isFileInputValid(input)) {\n                        return true;\n                    }\n\n                    // Note that checkValidity also takes care of the case where\n                    // the input is disabled, in which case, it is considered\n                    // valid (as the data will not be sent anyway).\n                    // This takes care of conditionally-hidden fields (whose\n                    // inputs are disabled while they are hidden) which should\n                    // not require validation while they are hidden. Indeed,\n                    // their purpose is to be able to enter additional data when\n                    // some condition is fulfilled. If such a field is required,\n                    // it is only required when visible for example.\n                    return !input.checkValidity();\n                });\n\n                // Update field color if invalid or erroneous\n                const $controls = $field.find('.form-control, .form-select, .form-check-input');\n                $field.removeClass('o_has_error');\n                $controls.removeClass('is-invalid');\n                if (invalid_inputs.length || error_fields[field_name]) {\n                    $field.addClass('o_has_error');\n                    $controls.addClass('is-invalid');\n                    if (typeof error_fields[field_name] === \"string\") {\n                        $field.popover({content: error_fields[field_name], trigger: 'hover', container: 'body', placement: 'top'});\n                        // update error message and show it.\n                        const popover = Popover.getInstance($field);\n                        popover._config.content = error_fields[field_name];\n                        $field.popover('show');\n                    }\n                    form_valid = false;\n                }\n            });\n            return form_valid;\n        },\n\n        update_status: function (status, message) {\n            if (status !== 'success') { // Restore send button behavior if result is an error\n                this.$el.find('.s_website_form_send, .o_website_form_send')\n                    .removeAttr('disabled')\n                    .removeClass('disabled'); // !compatibility\n                this.restoreBtnLoading();\n            }\n            var $result = this.$('#s_website_form_result, #o_website_form_result'); // !compatibility\n\n            if (status === 'error' && !message) {\n                message = _t(\"An error has occured, the form has not been sent.\");\n            }\n\n            // Note: we still need to wait that the widget is properly started\n            // before any qweb rendering which depends on xml assets\n            // because the event handlers are binded before the call to\n            // willStart for public widgets...\n            this.__started.then(() => $result.replaceWith(renderToElement(`website.s_website_form_status_${status}`, {\n                message: message,\n            })));\n        },\n\n        /**\n         * Checks if the file input is valid: if the number of files uploaded\n         * and their size do not exceed the limits that were set.\n         *\n         * @param {HTMLElement} inputEl an input of type file\n         * @returns {Boolean} true if the input is valid, false otherwise.\n         */\n        isFileInputValid(inputEl) {\n            // Note: the `maxFilesNumber` and `maxFileSize` data-attributes may\n            // not always be present, if the Form comes from an older version\n            // for example.\n\n            // Checking the number of files.\n            const maxFilesNumber = inputEl.dataset.maxFilesNumber;\n            if (maxFilesNumber && inputEl.files.length > maxFilesNumber) {\n                // Store information to display the error message later.\n                this.fileInputError = {type: \"number\", limit: maxFilesNumber};\n                return false;\n            }\n            // Checking the files size.\n            const maxFileSize = inputEl.dataset.maxFileSize; // in megabytes.\n            const bytesInMegabyte = 1_000_000;\n            if (maxFileSize) {\n                for (const file of Object.values(inputEl.files)) {\n                    if (file.size / bytesInMegabyte > maxFileSize) {\n                        this.fileInputError = {type: \"size\", limit: maxFileSize, fileName: file.name};\n                        return false;\n                    }\n                }\n            }\n            return true;\n        },\n\n        //----------------------------------------------------------------------\n        // Private\n        //----------------------------------------------------------------------\n\n        /**\n         * Gets the user's field needed to be fetched to pre-fill the form.\n         *\n         * @returns {string[]} List of user's field that have to be fetched.\n         */\n        _getUserPreFillFields() {\n            return ['name', 'phone', 'email', 'commercial_company_name'];\n        },\n        /**\n         * Compares the value with the comparable (and the between) with\n         * comparator as a means to compare\n         *\n         * @private\n         * @param {string} comparator The way that $value and $comparable have\n         *      to be compared\n         * @param {string} [value] The value of the field\n         * @param {string} [comparable] The value to compare\n         * @param {string} [between] The maximum date value in case comparator\n         *      is between or !between\n         * @returns {boolean}\n         */\n        _compareTo(comparator, value = '', comparable, between) {\n            // Value can be null when the compared field is supposed to be\n            // visible, but is not yet retrievable from the FormData() because\n            // the field was conditionally hidden. It can be considered empty.\n            if (value === null) {\n                value = '';\n            }\n\n            switch (comparator) {\n                case 'contains':\n                    return value.includes(comparable);\n                case '!contains':\n                    return !value.includes(comparable);\n                case 'equal':\n                case 'selected':\n                    return value === comparable;\n                case '!equal':\n                case '!selected':\n                    return value !== comparable;\n                case 'set':\n                    return value;\n                case '!set':\n                    return !value;\n                case 'greater':\n                    return parseFloat(value) > parseFloat(comparable);\n                case 'less':\n                    return parseFloat(value) < parseFloat(comparable);\n                case 'greater or equal':\n                    return parseFloat(value) >= parseFloat(comparable);\n                case 'less or equal':\n                    return parseFloat(value) <= parseFloat(comparable);\n                case 'fileSet':\n                    return value.name !== '';\n                case '!fileSet':\n                    return value.name === '';\n            }\n\n            const format = value.includes(':')\n                ? localization.dateTimeFormat\n                : localization.dateFormat;\n            // Date & Date Time comparison requires formatting the value\n            const dateTime = DateTime.fromFormat(value, format);\n            // If invalid, any value other than \"NaN\" would cause certain\n            // conditions to be broken.\n            value = dateTime.isValid ? dateTime.toUnixInteger() : NaN;\n\n            comparable = parseInt(comparable);\n            between = parseInt(between) || '';\n            switch (comparator) {\n                case 'dateEqual':\n                    return value === comparable;\n                case 'date!equal':\n                    return value !== comparable;\n                case 'before':\n                    return value < comparable;\n                case 'after':\n                    return value > comparable;\n                case 'equal or before':\n                    return value <= comparable;\n                case 'between':\n                    return value >= comparable && value <= between;\n                case '!between':\n                    return !(value >= comparable && value <= between);\n                case 'equal or after':\n                    return value >= comparable;\n            }\n        },\n        /**\n         * @private\n         * @param {HTMLElement} fieldEl the field we want to have a function\n         *      that calculates its visibility\n         * @returns {function} the function to be executed when we want to\n         *      recalculate the visibility of fieldEl\n         */\n        _buildVisibilityFunction(fieldEl) {\n            const visibilityCondition = fieldEl.dataset.visibilityCondition;\n            const dependencyName = fieldEl.dataset.visibilityDependency;\n            const comparator = fieldEl.dataset.visibilityComparator;\n            const between = fieldEl.dataset.visibilityBetween;\n            return () => {\n                // To be visible, at least one field with the dependency name must be visible.\n                const dependencyVisibilityFunction = this._visibilityFunctionByFieldName.get(dependencyName);\n                const dependencyIsVisible = !dependencyVisibilityFunction || dependencyVisibilityFunction();\n                if (!dependencyIsVisible) {\n                    return false;\n                }\n\n                const formData = new FormData(this.el);\n                const currentValueOfDependency = [\"contains\", \"!contains\"].includes(comparator)\n                    ? formData.getAll(dependencyName).join()\n                    : formData.get(dependencyName);\n                return this._compareTo(comparator, currentValueOfDependency, visibilityCondition, between);\n            };\n        },\n        /**\n         * Calculates the visibility for each field with conditional visibility\n         */\n        _updateFieldsVisibility() {\n            let anyFieldVisibilityUpdated = false;\n            for (const [fieldEl, visibilityFunction] of this._visibilityFunctionByFieldEl.entries()) {\n                const wasVisible = !fieldEl.closest(\".s_website_form_field\")\n                    .classList.contains(\"d-none\");\n                const isVisible = !!visibilityFunction();\n                this._updateFieldVisibility(fieldEl, isVisible);\n                anyFieldVisibilityUpdated |= wasVisible !== isVisible;\n            }\n            // Recursive check needed in case of a field (C) that\n            // conditionally displays a prefilled field (B), which in turn\n            // triggers a conditional visibility on another field (A),\n            // registered before B.\n            if (anyFieldVisibilityUpdated) {\n                this._updateFieldsVisibility();\n            }\n        },\n        /**\n         * Changes the visibility of a field.\n         *\n         * @param {HTMLElement} fieldEl\n         * @param {boolean} haveToBeVisible\n         */\n        _updateFieldVisibility(fieldEl, haveToBeVisible) {\n            const fieldContainerEl = fieldEl.closest('.s_website_form_field');\n            fieldContainerEl.classList.toggle('d-none', !haveToBeVisible);\n            for (const inputEl of fieldContainerEl.querySelectorAll('.s_website_form_input')) {\n                // Hidden inputs should also be disabled so that their data are\n                // not sent on form submit.\n                inputEl.disabled = !haveToBeVisible;\n            }\n        },\n        /**\n         * Creates a block containing the file name and a cross to delete it.\n         *\n         * @private\n         * @param {Object} fileDetails the details of the file being uploaded\n         * @param {HTMLElement} filesZoneEl the zone where the file blocks are\n         *      displayed\n         */\n        _createFileBlock(fileDetails, filesZoneEl) {\n            const fileBlockEl = renderToElement(\"website.file_block\", {fileName: fileDetails.name});\n            fileBlockEl.fileDetails = fileDetails;\n            filesZoneEl.append(fileBlockEl);\n        },\n        /**\n         * Creates the file upload button (= a button to replace the file input,\n         * in order to modify its text content more easily).\n         *\n         * @private\n         * @param {HTMLElement} inputEl the file input\n         */\n        _createAddFilesButton(inputEl) {\n            const addFilesButtonEl = document.createElement(\"INPUT\");\n            addFilesButtonEl.classList.add(\"o_add_files_button\", \"form-control\");\n            addFilesButtonEl.type = \"button\";\n            addFilesButtonEl.value = inputEl.hasAttribute(\"multiple\")\n                ? _t(\"Add Files\") : _t(\"Replace File\");\n            inputEl.parentNode.insertBefore(addFilesButtonEl, inputEl);\n            inputEl.classList.add(\"d-none\");\n        },\n\n        //----------------------------------------------------------------------\n        // Handlers\n        //----------------------------------------------------------------------\n\n        /**\n         * Calculates the visibility of the fields at each input event on the\n         * form (this method should be debounced in the start).\n         */\n        _onFieldInput() {\n            this._updateFieldsVisibility();\n        },\n        /**\n         * Called when files are uploaded: updates the button text content,\n         * displays the file blocks (containing the files name and a cross to\n         * delete them) and manages the files.\n         *\n         * @private\n         * @param {Event} ev\n         */\n        _onFileChange(ev) {\n            const fileInputEl = ev.currentTarget;\n            const fieldEl = fileInputEl.closest(\".s_website_form_field\");\n            const uploadedFiles = fileInputEl.files;\n            const addFilesButtonEl = fieldEl.querySelector(\".o_add_files_button\");\n\n            // The zone where the file blocks are displayed.\n            let filesZoneEl = fieldEl.querySelector(\".o_files_zone\");\n            // Update the button text content.\n            if (!addFilesButtonEl) {\n                this._createAddFilesButton(fileInputEl);\n            }\n\n            // Create a list to keep track of the files.\n            if (!fileInputEl.fileList) {\n                fileInputEl.fileList = new DataTransfer();\n            }\n\n            // If only one file can be uploaded, delete the previous file.\n            if (!fileInputEl.hasAttribute(\"multiple\") && uploadedFiles.length > 0) {\n                fileInputEl.fileList = new DataTransfer();\n                const fileBlockEl = fieldEl.querySelector(\".o_file_block\");\n                if (fileBlockEl) {\n                    fileBlockEl.remove();\n                }\n            }\n\n            // Add the uploaded files if they are not already there.\n            for (const newFile of uploadedFiles) {\n                if (![...fileInputEl.fileList.files].some(file => newFile.name === file.name &&\n                    newFile.size === file.size && newFile.type === file.type)) {\n                    fileInputEl.fileList.items.add(newFile);\n                    const fileDetails = {name: newFile.name, size: newFile.size, type: newFile.type};\n                    this._createFileBlock(fileDetails, filesZoneEl);\n                }\n            }\n            // Update the input files.\n            fileInputEl.files = fileInputEl.fileList.files;\n        },\n        /**\n         * Called when a file is deleted by clicking on the cross on the block\n         * describing it.\n         *\n         * @private\n         * @param {Event} ev\n         */\n        _onFileDeleteClick(ev) {\n            const fileBlockEl = ev.target.closest(\".o_file_block\");\n            const fieldEl = fileBlockEl.closest(\".s_website_form_field\");\n            const fileInputEl = fieldEl.querySelector(\"input[type=file]\");\n            const fileDetails = fileBlockEl.fileDetails;\n            const addFilesButtonEl = fieldEl.querySelector(\".o_add_files_button\");\n\n            // Create a new file list containing the remaining files.\n            const newFileList = new DataTransfer();\n            for (const file of Object.values(fileInputEl.fileList.files)) {\n                if (file.name !== fileDetails.name || file.size !== fileDetails.size\n                    || file.type !== fileDetails.type) {\n                    newFileList.items.add(file);\n                }\n            }\n            // Update the input lists and remove the file block.\n            Object.assign(fileInputEl, {fileList: newFileList, files: newFileList.files});\n            fileBlockEl.remove();\n\n            // Restore the file input if there are no files uploaded and update\n            // the fields visibility.\n            if (!newFileList.files.length) {\n                fileInputEl.classList.remove(\"d-none\");\n                addFilesButtonEl.remove();\n                this._updateFieldsVisibility();\n            }\n        },\n        /**\n         * Detects when the fake input file button is clicked to simulate a\n         * click on the real input.\n         *\n         * @private\n         * @param {MouseEvent} ev\n         */\n        _onAddFilesButtonClick(ev) {\n            const fileInputEl = ev.target.parentNode.querySelector(\"input[type=file]\");\n            fileInputEl.click();\n        },\n    });\n", "/** @odoo-module **/\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport publicWidget from '@web/legacy/js/public/public_widget';\n\nimport { isBrowserSafari } from \"@web/core/browser/feature_detection\";\nimport { renderToElement, renderToString } from \"@web/core/utils/render\";\nimport { debounce } from '@web/core/utils/timing';\n\nimport { markup } from \"@odoo/owl\";\n\npublicWidget.registry.searchBar = publicWidget.Widget.extend({\n    selector: '.o_searchbar_form',\n    events: {\n        'input .search-query': '_onInput',\n        'focusout': '_onFocusOut',\n        \"mousedown .o_dropdown_menu .dropdown-item\": \"_onMousedown\",\n        \"mouseup .o_dropdown_menu .dropdown-item\": \"_onMouseup\",\n        'keydown .search-query, .dropdown-item': '_onKeydown',\n        'search .search-query': '_onSearch',\n    },\n    autocompleteMinWidth: 300,\n\n    /**\n     * @constructor\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n\n        this.keepLast = new KeepLast();\n\n        this._onInput = debounce(this._onInput, 400);\n        this._onFocusOut = debounce(this._onFocusOut, 100);\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        this.$input = this.$('.search-query');\n\n        this.searchType = this.$input.data('searchType');\n        this.order = this.$('.o_search_order_by').val();\n        this.limit = parseInt(this.$input.data('limit'));\n        this.displayDescription = this.$input.data('displayDescription');\n        this.displayExtraLink = this.$input.data('displayExtraLink');\n        this.displayDetail = this.$input.data('displayDetail');\n        this.displayImage = this.$input.data('displayImage');\n        this.wasEmpty = !this.$input.val();\n        // Make it easy for customization to disable fuzzy matching on specific searchboxes\n        this.allowFuzzy = !this.$input.data('noFuzzy');\n        if (this.limit) {\n            this.$input.attr('autocomplete', 'off');\n        }\n\n        this.options = {\n            'displayImage': this.displayImage,\n            'displayDescription': this.displayDescription,\n            'displayExtraLink': this.displayExtraLink,\n            'displayDetail': this.displayDetail,\n            'allowFuzzy': this.allowFuzzy,\n        };\n        const form = this.$('.o_search_order_by').parents('form');\n        for (const field of form.find(\"input[type='hidden']\")) {\n            this.options[field.name] = field.value;\n        }\n        const action = form.attr('action') || window.location.pathname + window.location.search;\n        const [urlPath, urlParams] = action.split('?');\n        if (urlParams) {\n            for (const keyValue of urlParams.split('&')) {\n                const [key, value] = keyValue.split('=');\n                if (value && key !== 'search') {\n                    // Decode URI parameters: revert + to space then decodeURIComponent.\n                    this.options[decodeURIComponent(key.replace(/\\+/g, '%20'))] = decodeURIComponent(value.replace(/\\+/g, '%20'));\n                }\n            }\n        }\n        const pathParts = urlPath.split('/');\n        for (const index in pathParts) {\n            const value = decodeURIComponent(pathParts[index]);\n            if (index > 0 && /-[0-9]+$/.test(value)) { // is sluggish\n                this.options[decodeURIComponent(pathParts[index - 1])] = value;\n            }\n        }\n\n        if (this.$input.data('noFuzzy')) {\n            $(\"<input type='hidden' name='noFuzzy' value='true'/>\").appendTo(this.$input);\n        }\n        return this._super.apply(this, arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        this._render(null);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _adaptToScrollingParent() {\n        const bcr = this.el.getBoundingClientRect();\n        this.$menu[0].style.setProperty('position', 'fixed', 'important');\n        this.$menu[0].style.setProperty('top', `${bcr.bottom}px`, 'important');\n        this.$menu[0].style.setProperty('left', `${bcr.left}px`, 'important');\n        this.$menu[0].style.setProperty('max-width', `${bcr.width}px`, 'important');\n        this.$menu[0].style.setProperty('max-height', `${document.body.clientHeight - bcr.bottom - 16}px`, 'important');\n    },\n    /**\n     * @private\n     */\n    async _fetch() {\n        const res = await rpc('/website/snippet/autocomplete', {\n            'search_type': this.searchType,\n            'term': this.$input.val(),\n            'order': this.order,\n            'limit': this.limit,\n            'max_nb_chars': Math.round(Math.max(this.autocompleteMinWidth, parseInt(this.$el.width())) * 0.22),\n            'options': this.options,\n        });\n        const fieldNames = this._getFieldsNames();\n        res.results.forEach(record => {\n            for (const fieldName of fieldNames) {\n                if (record[fieldName]) {\n                    record[fieldName] = markup(record[fieldName]);\n                }\n            }\n        });\n        return res;\n    },\n    /**\n     * @private\n     */\n    _render: function (res) {\n        if (this._scrollingParentEl) {\n            this._scrollingParentEl.removeEventListener('scroll', this._menuScrollAndResizeHandler);\n            window.removeEventListener('resize', this._menuScrollAndResizeHandler);\n            delete this._scrollingParentEl;\n            delete this._menuScrollAndResizeHandler;\n        }\n\n        let pageScrollHeight = null;\n        const $prevMenu = this.$menu;\n        if (res && this.limit) {\n            const results = res['results'];\n            let template = 'website.s_searchbar.autocomplete';\n            const candidate = template + '.' + this.searchType;\n            if (candidate in renderToString.app.rawTemplates) {\n                template = candidate;\n            }\n            this.$menu = $(renderToElement(template, {\n                results: results,\n                parts: res['parts'],\n                hasMoreResults: results.length < res['results_count'],\n                search: this.$input.val(),\n                fuzzySearch: res['fuzzy_search'],\n                widget: this,\n            }));\n            this.$menu.css('min-width', this.autocompleteMinWidth);\n\n            // Handle the case where the searchbar is in a mega menu by making\n            // it position:fixed and forcing its size. Note: this could be the\n            // default behavior or at least needed in more cases than the mega\n            // menu only (all scrolling parents). But as a stable fix, it was\n            // easier to fix that case only as a first step, especially since\n            // this cannot generically work on all scrolling parent.\n            const megaMenuEl = this.el.closest('.o_mega_menu');\n            if (megaMenuEl) {\n                const navbarEl = this.el.closest('.navbar');\n                const navbarTogglerEl = navbarEl ? navbarEl.querySelector('.navbar-toggler') : null;\n                if (navbarTogglerEl && navbarTogglerEl.clientWidth < 1) {\n                    this._scrollingParentEl = megaMenuEl;\n                    this._menuScrollAndResizeHandler = () => this._adaptToScrollingParent();\n                    this._scrollingParentEl.addEventListener('scroll', this._menuScrollAndResizeHandler);\n                    window.addEventListener('resize', this._menuScrollAndResizeHandler);\n\n                    this._adaptToScrollingParent();\n                }\n            }\n\n            pageScrollHeight = document.documentElement.scrollHeight;\n            this.$el.append(this.$menu);\n\n            this.$el.find('button.extra_link').on('click', function (event) {\n                event.preventDefault();\n                window.location.href = event.currentTarget.dataset['target'];\n            });\n            this.$el.find('.s_searchbar_fuzzy_submit').on('click', (event) => {\n                event.preventDefault();\n                this.$input.val(res['fuzzy_search']);\n                const form = this.$('.o_search_order_by').parents('form');\n                form.submit();\n            });\n        }\n\n        this.$el.toggleClass('dropdown show', !!res);\n        if ($prevMenu) {\n            $prevMenu.remove();\n        }\n        // Adjust the menu's position based on the scroll height.\n        if (res && this.limit) {\n            this.el.classList.remove(\"dropup\");\n            delete this.$menu[0].dataset.bsPopper;\n            if (document.documentElement.scrollHeight > pageScrollHeight) {\n                // If the menu overflows below the page, we reduce its height.\n                this.$menu[0].style.maxHeight = \"40vh\";\n                this.$menu[0].style.overflowY = \"auto\";\n                // We then recheck if the menu still overflows below the page.\n                if (document.documentElement.scrollHeight > pageScrollHeight) {\n                    // If the menu still overflows below the page after its height\n                    // has been reduced, we position it above the input.\n                    this.el.classList.add(\"dropup\");\n                    this.$menu[0].dataset.bsPopper = \"\";\n                }\n            }\n        }\n    },\n    _getFieldsNames() {\n        return [\n            'description',\n            'detail',\n            'detail_extra',\n            'detail_strike',\n            'extra_link',\n            'name',\n        ];\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onInput: function () {\n        if (!this.limit) {\n            return;\n        }\n        if (this.searchType === 'all' && !this.$input.val().trim().length) {\n            this._render();\n        } else {\n            this.keepLast.add(this._fetch()).then(this._render.bind(this));\n        }\n    },\n    /**\n     * @private\n     */\n    _onFocusOut: function () {\n        if (!this.linkHasFocus && !this.$el.has(document.activeElement).length) {\n            this._render();\n        }\n    },\n    _onMousedown(ev) {\n        // On Safari, links and buttons are not focusable by default. We need\n        // to get around that behavior to avoid _onFocusOut() from triggering\n        // _render(), as this would prevent the click from working.\n        if (isBrowserSafari) {\n            this.linkHasFocus = true;\n        }\n    },\n    _onMouseup(ev) {\n        // See comment in _onMousedown.\n        if (isBrowserSafari) {\n            this.linkHasFocus = false;\n        }\n    },\n    /**\n     * @private\n     */\n    _onKeydown: function (ev) {\n        switch (ev.key) {\n            case \"Escape\":\n                this._render();\n                break;\n            case \"ArrowUp\":\n            case \"ArrowDown\":\n                ev.preventDefault();\n                if (this.$menu) {\n                    const focusableEls = [this.$input[0], ...this.$menu[0].children];\n                    const focusedEl = document.activeElement;\n                    const currentIndex = focusableEls.indexOf(focusedEl) || 0;\n                    const delta = ev.key === \"ArrowUp\" ? focusableEls.length - 1 : 1;\n                    const nextIndex = (currentIndex + delta) % focusableEls.length;\n                    const nextFocusedEl = focusableEls[nextIndex];\n                    nextFocusedEl.focus();\n                }\n                break;\n            case \"Enter\":\n                this.limit = 0; // prevent autocomplete\n                break;\n        }\n    },\n    /**\n     * @private\n     */\n    _onSearch: function (ev) {\n        if (this.$input[0].value) { // actual search\n            this.limit = 0; // prevent autocomplete\n        } else { // clear button clicked\n            this._render(); // remove existing suggestions\n            ev.preventDefault();\n            if (!this.wasEmpty) {\n                this.limit = 0; // prevent autocomplete\n                const form = this.$('.o_search_order_by').parents('form');\n                form.submit();\n            }\n        }\n    },\n});\n\nexport default {\n    searchBar: publicWidget.registry.searchBar,\n};\n", "import publicWidget from '@web/legacy/js/public/public_widget';\nimport DynamicSnippet from '@website/snippets/s_dynamic_snippet/000';\nimport { Domain } from \"@web/core/domain\";\n\n\nconst AppointmentsListSnippet = DynamicSnippet.extend({\n    selector: '.s_appointments',\n    disabledInEditableMode: false,\n    /**\n     * @override\n     * @private\n     */\n    _getSearchDomain: function () {\n        let searchDomain = new Domain(this._super(...arguments));\n        const snippetDataset = this.el.dataset;\n        const filterType = snippetDataset.filterType;\n        const appointmentNames = (snippetDataset.appointmentNames || '')\n            .split(',')\n            .map((name) => name.trim())\n            .filter((name) => name.length > 0);\n\n        if (filterType === 'users') {\n            searchDomain = Domain.and([searchDomain, [['schedule_based_on', '=', 'users']]]);\n            if (snippetDataset.filterUsers) {\n                const filterUserIds = JSON.parse(snippetDataset.filterUsers).map(u => u.id);\n                if (filterUserIds.length !== 0) {\n                    searchDomain = Domain.and([searchDomain, [['staff_user_ids', 'in', filterUserIds]]]);\n                }\n            }\n        } else if (filterType === 'resources') {\n            searchDomain = Domain.and([searchDomain, [['schedule_based_on', '=', 'resources']]]);\n            if (snippetDataset.filterResources) {\n                const filterResourceIds = JSON.parse(snippetDataset.filterResources).map(r => r.id);\n                if (filterResourceIds.length !== 0) {\n                    searchDomain = Domain.and([searchDomain, [['resource_ids', 'in', filterResourceIds]]]);\n                }\n            }\n        }\n        if (appointmentNames.length > 0) {\n            const nameDomains = appointmentNames.map((name) => [['name', 'ilike', name]]);\n            searchDomain = Domain.and([searchDomain, Domain.or(nameDomains)]);\n        }\n        return searchDomain.toList();\n    },\n    /**\n     * @override\n     * @private\n     */\n    _getMainPageUrl() {\n        return \"/appointment\";\n    },\n});\n\npublicWidget.registry.s_appointments = AppointmentsListSnippet;\n\nexport default AppointmentsListSnippet;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\n\nconst OnlineAppointmentCtaWidget = publicWidget.Widget.extend({\n    selector: '.s_online_appointment',\n    disabledInEditableMode: true,\n    events: {\n        'click': '_onCtaClick'\n    },\n    _onCtaClick: function (ev) {\n        let url = '/appointment';\n\n        const selectedAppointments = ev.target.closest('.s_online_appointment').dataset.appointmentTypes;\n        const appointmentsTypeIds = selectedAppointments ? JSON.parse(selectedAppointments) : [];\n        const nbSelectedAppointments = appointmentsTypeIds.length;\n        if (nbSelectedAppointments === 1) {\n            url += `/${encodeURIComponent(appointmentsTypeIds[0])}`;\n            const selectedUsers = ev.target.closest('.s_online_appointment').dataset.staffUsers;\n            if (JSON.parse(selectedUsers).length) {\n                url += `?filter_staff_user_ids=${encodeURIComponent(selectedUsers)}`;\n            }\n        } else if (nbSelectedAppointments > 1) {\n            url += `?filter_appointment_type_ids=${encodeURIComponent(selectedAppointments)}`;\n        }\n        window.location = url;\n    },\n});\n\npublicWidget.registry.online_appointment = OnlineAppointmentCtaWidget;\n\nexport default OnlineAppointmentCtaWidget;\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\n\npublicWidget.registry.searchBar.include({\n\n    /**\n     * Allows to keep the invite token and the filters in the URL\n     * parameters after clicking on the search bar suggestions.\n     *\n     * @override\n     */\n    _render: function (res) {\n        if (res && this.searchType === 'appointments' && res.parts.website_url) {\n            res.results.forEach(result => {\n                result.website_url = `${result.website_url}${location.search}`;\n            })\n        }\n        this._super(...arguments);\n    }\n});\n", "import { formatCurrency } from \"@web/core/currency\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { rpc } from \"@web/core/network/rpc\";\n\nconst CUSTOM_BUTTON_EXTRA_WIDTH = 10;\nlet cachedCurrency;\n\npublicWidget.registry.DonationSnippet = publicWidget.Widget.extend({\n    selector: '.s_donation',\n    disabledInEditableMode: false,\n    events: {\n        'click .s_donation_btn': '_onClickPrefilledButton',\n        'click .s_donation_donate_btn': '_onClickDonateNowButton',\n        'input #s_donation_range_slider': '_onInputRangeSlider',\n    },\n\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n        this.$rangeSlider = this.$('#s_donation_range_slider');\n        this.defaultAmount = this.el.dataset.defaultAmount;\n        if (this.$rangeSlider.length) {\n            this.$rangeSlider.val(this.defaultAmount);\n            this._setBubble(this.$rangeSlider);\n        }\n        await this._displayCurrencies();\n        const customButtonEl = this.el.querySelector(\"#s_donation_amount_input\");\n        if (customButtonEl) {\n            const canvasEl = document.createElement(\"canvas\");\n            const context = canvasEl.getContext(\"2d\");\n            context.font = window.getComputedStyle(customButtonEl).font;\n            const width = context.measureText(customButtonEl.placeholder).width;\n            customButtonEl.style.maxWidth = `${Math.ceil(width) + CUSTOM_BUTTON_EXTRA_WIDTH}px`;\n        }\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        const customButtonEl = this.el.querySelector(\"#s_donation_amount_input\");\n        if (customButtonEl) {\n            customButtonEl.style.maxWidth = \"\";\n        }\n        this.$el.find('.s_donation_currency').remove();\n        this._deselectPrefilledButtons();\n        this.$('.alert-danger').remove();\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _deselectPrefilledButtons() {\n        this.$('.s_donation_btn').removeClass('active');\n    },\n    /**\n     * @private\n     * @param {jQuery} $range\n     */\n    _setBubble($range) {\n        const $bubble = this.$('.s_range_bubble');\n        const val = $range.val();\n        const min = $range[0].min || 0;\n        const max = $range[0].max || 100;\n        const newVal = Number(((val - min) * 100) / (max - min));\n        const tipOffsetLow = 8 - (newVal * 0.16); // the range thumb size is 16px*16px. The '8' and the '0.16' are related to that 16px (50% and 1% of 16px)\n        $bubble.contents().filter(function () {\n            return this.nodeType === 3;\n        }).replaceWith(val);\n\n        // Sorta magic numbers based on size of the native UI thumb (source: https://css-tricks.com/value-bubbles-for-range-inputs/)\n        $bubble[0].style.left = `calc(${newVal}% + (${tipOffsetLow}px))`;\n    },\n    /**\n     * @private\n     */\n    _displayCurrencies() {\n        return this._getCachedCurrency().then((result) => {\n            // No need to recreate the elements if the currency is already set.\n            if (this.currency === result) {\n                return;\n            }\n            this.currency = result;\n            this.$('.s_donation_currency').remove();\n            const $prefilledButtons = this.$('.s_donation_btn, .s_range_bubble');\n            $prefilledButtons.toArray().forEach((button) => {\n                const before = result.position === \"before\";\n                const $currencySymbol = document.createElement('span');\n                $currencySymbol.innerText = result.symbol;\n                $currencySymbol.classList.add('s_donation_currency', before ? \"pe-1\" : \"ps-1\");\n                if (before) {\n                    $(button).prepend($currencySymbol);\n                } else {\n                    $(button).append($currencySymbol);\n                }\n            });\n        });\n    },\n    /**\n     * @private\n     */\n    _getCachedCurrency() {\n        return cachedCurrency\n            ? Promise.resolve(cachedCurrency)\n            : rpc(\"/website/get_current_currency\").then((result) => {\n                cachedCurrency = result;\n                return result;\n            });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onClickPrefilledButton(ev) {\n        const $button = $(ev.currentTarget);\n        this._deselectPrefilledButtons();\n        $button.addClass('active');\n        if (this.$rangeSlider.length) {\n            this.$rangeSlider.val($button[0].dataset.donationValue);\n            this._setBubble(this.$rangeSlider);\n        }\n    },\n    /**\n     * @private\n     */\n    _onClickDonateNowButton(ev) {\n        if (this.editableMode) {\n            return;\n        };\n        this.$('.alert-danger').remove();\n        const $buttons = this.$('.s_donation_btn');\n        const $selectedButton = $buttons.filter('.active');\n        let amount = $selectedButton.length ? $selectedButton[0].dataset.donationValue : 0;\n        if (this.el.dataset.displayOptions && !amount) {\n            if (this.$rangeSlider.length) {\n                amount = this.$rangeSlider.val();\n            } else if ($buttons.length) {\n                amount = parseFloat(this.$('#s_donation_amount_input').val());\n                let errorMessage = '';\n                const minAmount = parseFloat(this.el.dataset.minimumAmount);\n                if (!amount) {\n                    errorMessage = _t(\"Please select or enter an amount\");\n                } else if (amount < minAmount) {\n                    errorMessage = _t(\n                        \"The minimum donation amount is %(amount)s\",\n                        {\n                            amount: formatCurrency(minAmount, this.currency.id),\n                        }\n                    );\n                }\n                if (errorMessage) {\n                    $(ev.currentTarget).before($('<p>', {\n                        class: 'alert alert-danger',\n                        text: errorMessage,\n                    }));\n                    return;\n                }\n            }\n        }\n        if (!amount) {\n            amount = this.defaultAmount;\n        }\n        const $form = this.$('.s_donation_form');\n        $('<input>').attr({type: 'hidden', name: 'amount', value: amount}).appendTo($form);\n        $('<input>').attr({type: 'hidden', name: 'currency_id', value: this.currency.id}).appendTo($form);\n        $('<input>').attr({type: 'hidden', name: 'csrf_token', value: odoo.csrf_token}).appendTo($form);\n        $('<input>').attr({type: 'hidden', name: 'donation_options', value: JSON.stringify(this.el.dataset)}).appendTo($form);\n        $form.submit();\n    },\n    /**\n     * @private\n     */\n    _onInputRangeSlider(ev) {\n        this._deselectPrefilledButtons();\n        this._setBubble($(ev.currentTarget));\n    },\n});\n\nexport default {\n    DonationSnippet: publicWidget.registry.DonationSnippet,\n};\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport { rpc } from \"@web/core/network/rpc\";\nimport { cartHandlerMixin } from '@website_sale/js/website_sale_utils';\nimport { WebsiteSale } from '@website_sale/js/website_sale';\nimport { _t } from \"@web/core/l10n/translation\";\n\npublicWidget.registry.AddToCartSnippet = WebsiteSale.extend(cartHandlerMixin, {\n    selector: '.s_add_to_cart_btn',\n    events: {\n        'click': '_onClickAddToCartButton',\n    },\n\n    init() {\n        this._super(...arguments);\n        this.notification = this.bindService(\"notification\");\n    },\n\n    _onClickAddToCartButton: async function (ev) {\n        const dataset = ev.currentTarget.dataset;\n\n        const action = dataset.action;\n        const productId = parseInt(dataset.productVariantId);\n        const productTemplateId = parseInt(dataset.productTemplateId);\n        const isCombo = dataset.isCombo;\n\n        if (!productId || isCombo) {\n            this.rootProduct = {\n                product_id: productId,\n                product_template_id: productTemplateId,\n                quantity: 1,\n                product_custom_attribute_values: [],\n                variant_values: [],\n                no_variant_attribute_values: [],\n            };\n            this._onProductReady();\n        } else {\n            const isAddToCartAllowed = await rpc(`/shop/product/is_add_to_cart_allowed`, {\n                product_id: productId,\n            });\n            if (!isAddToCartAllowed) {\n                this.notification.add(\n                    _t('This product does not exist therefore it cannot be added to cart.'),\n                    { title: 'User Error', type: 'warning' }\n                );\n                return;\n            }\n            this.isBuyNow = action === 'buy_now';\n            this.stayOnPageOption = !this.isBuyNow;\n            this.addToCart({product_id: productId, add_qty: 1});\n        }\n    },\n});\n\nexport default publicWidget.registry.AddToCartSnippet;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport DynamicSnippetCarousel from \"@website/snippets/s_dynamic_snippet_carousel/000\";\nimport wSaleUtils from \"@website_sale/js/website_sale_utils\";\nimport { WebsiteSale } from \"../../js/website_sale\";\n\nconst DynamicSnippetProducts = DynamicSnippetCarousel.extend({\n    selector: '.s_dynamic_snippet_products',\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Gets the category search domain\n     *\n     * @private\n     */\n    _getCategorySearchDomain() {\n        const searchDomain = [];\n        let productCategoryId = this.$el.get(0).dataset.productCategoryId;\n        if (productCategoryId && productCategoryId !== 'all') {\n            if (productCategoryId === 'current') {\n                productCategoryId = undefined;\n                const productCategoryField = $(\"#product_details\").find(\".product_category_id\");\n                if (productCategoryField && productCategoryField.length) {\n                    productCategoryId = parseInt(productCategoryField[0].value);\n                }\n                if (!productCategoryId) {\n                    this.trigger_up('main_object_request', {\n                        callback: function (value) {\n                            if (value.model === \"product.public.category\") {\n                                productCategoryId = value.id;\n                            }\n                        },\n                    });\n                }\n                if (!productCategoryId) {\n                    // Try with categories from product, unfortunately the category hierarchy is not matched with this approach\n                    const productTemplateId = $(\"#product_details\").find(\".product_template_id\");\n                    if (productTemplateId && productTemplateId.length) {\n                        searchDomain.push(['public_categ_ids.product_tmpl_ids', '=', parseInt(productTemplateId[0].value)]);\n                    }\n                }\n            }\n            if (productCategoryId) {\n                searchDomain.push(['public_categ_ids', 'child_of', parseInt(productCategoryId)]);\n            }\n        }\n        return searchDomain;\n    },\n    /**\n     * Gets the tag search domain\n     *\n     * @private\n     */\n    _getTagSearchDomain() {\n        const searchDomain = [];\n        let productTagIds = this.$el.get(0).dataset.productTagIds;\n        productTagIds = productTagIds ? JSON.parse(productTagIds) : [];\n        if (productTagIds.length) {\n            searchDomain.push(['all_product_tag_ids', 'in', productTagIds.map(productTag => productTag.id)]);\n        }\n        return searchDomain;\n    },\n    /**\n     * Method to be overridden in child components in order to provide a search\n     * domain if needed.\n     * @override\n     * @private\n     */\n    _getSearchDomain: function () {\n        const searchDomain = this._super.apply(this, arguments);\n        searchDomain.push(...this._getCategorySearchDomain());\n        searchDomain.push(...this._getTagSearchDomain());\n        const productNames = this.$el.get(0).dataset.productNames;\n        if (productNames) {\n            const nameDomain = [];\n            for (const productName of productNames.split(',')) {\n                // Ignore empty names\n                if (!productName.length) {\n                    continue;\n                }\n                // Search on name, internal reference and barcode.\n                if (nameDomain.length) {\n                    nameDomain.unshift('|');\n                }\n                nameDomain.push(...[\n                    '|', '|', ['name', 'ilike', productName],\n                              ['default_code', '=', productName],\n                              ['barcode', '=', productName],\n                ]);\n            }\n            searchDomain.push(...nameDomain);\n        }\n        if (!this.el.dataset.showVariants) {\n            searchDomain.push('hide_variants')\n        }\n        return searchDomain;\n    },\n    /**\n     * @override\n     */\n    _getRpcParameters: function () {\n        const productTemplateId = $(\"#product_details\").find(\".product_template_id\");\n        return Object.assign(this._super.apply(this, arguments), {\n            productTemplateId: productTemplateId && productTemplateId.length ? productTemplateId[0].value : undefined,\n        });\n    },\n    /**\n     * @override\n     * @private\n     */\n    _getMainPageUrl() {\n        return \"/shop\";\n    },\n});\n\nconst DynamicSnippetProductsCard = WebsiteSale.extend({\n    selector: '.o_carousel_product_card',\n    read_events: {\n        'click .js_add_cart': '_onClickAddToCart',\n        'click .js_remove': '_onRemoveFromRecentlyViewed',\n    },\n\n    init(root, options) {\n        const parent = options.parent || root;\n        this._super(parent, options);\n    },\n\n    start() {\n        this.add2cartRerender = this.el.dataset.add2cartRerender === 'True';\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Event triggered by a click on the Add to cart button\n     *\n     * @param {OdooEvent} ev\n     */\n    async _onClickAddToCart(ev) {\n        const button = ev.currentTarget\n        if (!button.dataset.productSelected || button.dataset.isCombo) {\n            const dummy_form = document.createElement('form');\n            dummy_form.setAttribute('method', 'post');\n            dummy_form.setAttribute('action', '/shop/cart/update');\n\n            const inputPT = document.createElement('input');\n            inputPT.setAttribute('name', 'product_template_id');\n            inputPT.setAttribute('type', 'hidden');\n            inputPT.setAttribute('value', button.dataset.productTemplateId);\n            dummy_form.appendChild(inputPT);\n\n            const inputPP = document.createElement('input');\n            inputPP.setAttribute('name', 'product_id');\n            inputPP.setAttribute('type', 'hidden');\n            inputPP.setAttribute('value', button.dataset.productId);\n            dummy_form.appendChild(inputPP);\n\n            return this._handleAdd($(dummy_form));  // existing logic expects jquery form\n        }\n        else {\n            const data = await rpc(\"/shop/cart/update_json\", {\n                product_id: parseInt(ev.currentTarget.dataset.productId),\n                add_qty: 1,\n                display: false,\n            });\n            wSaleUtils.updateCartNavBar(data);\n            wSaleUtils.showCartNotification(this.call.bind(this), data.notification_info);\n        }\n        if (this.add2cartRerender) {\n            this.trigger_up('widgets_start_request', {\n                $target: this.$el.closest('.s_dynamic'),\n            });\n        }\n    },\n    /**\n     * Event triggered by a click on the remove button on a \"recently viewed\"\n     * template.\n     *\n     * @param {OdooEvent} ev\n     */\n    async _onRemoveFromRecentlyViewed(ev) {\n        const rpcParams = {}\n        if (ev.currentTarget.dataset.productSelected) {\n            rpcParams.product_id = ev.currentTarget.dataset.productId;\n        } else {\n            rpcParams.product_template_id = ev.currentTarget.dataset.productTemplateId;\n        }\n        await rpc(\"/shop/products/recently_viewed_delete\", rpcParams);\n        this.trigger_up('widgets_start_request', {\n            $target: this.$el.closest('.s_dynamic'),\n        });\n    },\n});\n\npublicWidget.registry.dynamic_snippet_products_cta = DynamicSnippetProductsCard;\npublicWidget.registry.dynamic_snippet_products = DynamicSnippetProducts;\n\nexport default DynamicSnippetProducts;\n", "/** @odoo-module **/\n\nimport PopupWidget from '@website/snippets/s_popup/000';\n\nPopupWidget.include({\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Checks if the given primary button should allow or not to close the\n     * modal.\n     *\n     * @override\n     */\n    _canBtnPrimaryClosePopup(primaryBtnEl) {\n        return (\n            this._super(...arguments)\n            && !primaryBtnEl.classList.contains(\"js_add_cart\")\n        );\n    },\n});\n\nexport default PopupWidget;\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\n\npublicWidget.registry.RippleEffect = publicWidget.Widget.extend({\n    selector: '.btn, .dropdown-toggle, .dropdown-item',\n    events: {\n        'click': '_onClick',\n    },\n    duration: 350,\n\n    /**\n     * @override\n     */\n    destroy: function () {\n        this._super(...arguments);\n        if (this.rippleEl) {\n            this.rippleEl.remove();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {boolean} [toggle]\n     */\n    _toggleRippleEffect: function (toggle) {\n        this.el.classList.toggle('o_js_ripple_effect', toggle);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onClick: function (ev) {\n        if (!this.rippleEl) {\n            this.rippleEl = document.createElement('span');\n            this.rippleEl.classList.add('o_ripple_item');\n            this.rippleEl.style.animationDuration = `${this.duration}ms`;\n            this.el.appendChild(this.rippleEl);\n        }\n\n        clearTimeout(this.timeoutID);\n        this._toggleRippleEffect(false);\n\n        const offsetY = this.$el.offset().top;\n        const offsetX = this.$el.offset().left;\n        // The diameter need to be recomputed because a change of window width\n        // can affect the size of a button (e.g. media queries).\n        const diameter = Math.max(this.$el.outerWidth(), this.$el.outerHeight());\n\n        this.rippleEl.style.width = `${diameter}px`;\n        this.rippleEl.style.height = `${diameter}px`;\n        this.rippleEl.style.top = `${ev.pageY - offsetY - diameter / 2}px`;\n        this.rippleEl.style.left = `${ev.pageX - offsetX - diameter / 2}px`;\n\n        this._toggleRippleEffect(true);\n        this.timeoutID = setTimeout(() => this._toggleRippleEffect(false), this.duration);\n    },\n});\n", "/** @odoo-module **/\n\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport DynamicSnippet from \"@website/snippets/s_dynamic_snippet/000\";\n\nconst DynamicSnippetBlogPosts = DynamicSnippet.extend({\n    selector: '.s_dynamic_snippet_blog_posts',\n    disabledInEditableMode: false,\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Method to be overridden in child components in order to provide a search\n     * domain if needed.\n     * @override\n     * @private\n     */\n    _getSearchDomain: function () {\n        const searchDomain = this._super.apply(this, arguments);\n        const filterByBlogId = parseInt(this.$el.get(0).dataset.filterByBlogId);\n        if (filterByBlogId >= 0) {\n            searchDomain.push(['blog_id', '=', filterByBlogId]);\n        }\n        return searchDomain;\n    },\n    /**\n     * @override\n     * @private\n     */\n    _getMainPageUrl() {\n        return \"/blog\";\n    },\n});\npublicWidget.registry.blog_posts = DynamicSnippetBlogPosts;\n\nexport default DynamicSnippetBlogPosts;\n", "/** @odoo-module **/\n\nimport { groupBy } from '@web/core/utils/arrays';\nimport publicWidget from '@web/legacy/js/public/public_widget';\nimport DynamicSnippet from '@website/snippets/s_dynamic_snippet/000';\n\nconst DynamicSnippetEvents = DynamicSnippet.extend({\n    // While the selector has 'upcoming_snippet' in its name, it now has a filter\n    // option to include ongoing events. The name is kept for backward compatibility.\n    selector: '.s_event_upcoming_snippet',\n    disabledInEditableMode: false,\n\n    /**\n     * @override\n     * @private\n     */\n    _getSearchDomain: function () {\n        let searchDomain = this._super.apply(this, arguments);\n        const filterByTagIds = this.$el.get(0).dataset.filterByTagIds;\n        if (filterByTagIds) {\n            let tagGroupedByCategory = groupBy(JSON.parse(filterByTagIds), 'category_id');\n            for (const category in tagGroupedByCategory) {\n                searchDomain = searchDomain.concat(\n                    [['tag_ids', 'in', tagGroupedByCategory[category].map(e => e.id)]]);\n            }\n        }\n        return searchDomain;\n    },\n    /**\n     * @override\n     * @private\n     */\n    _getMainPageUrl() {\n        return \"/event\";\n    },\n});\n\npublicWidget.registry.events = DynamicSnippetEvents;\n", "/** @odoo-module **/\n\nimport publicWidget from '@web/legacy/js/public/public_widget';\n\npublicWidget.registry.searchBar.include({\n    /**\n     *\n     * @override\n     */\n    _getFieldsNames() {\n        return [...this._super(), 'address_name'];\n    }\n});\n", "/** @odoo-module **/\n\nimport PopupWidget from '@website/snippets/s_popup/000';\n\nPopupWidget.include({\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Prevents the (newsletter) popup to be shown if the user is subscribed.\n     *\n     * @override\n     */\n    _canShowPopup() {\n        if (\n            this.$el.is('.o_newsletter_popup') &&\n            this.$el.find('input.js_subscribe_value, input.js_subscribe_email').prop('disabled') // js_subscribe_email is kept by compatibility (it was the old name of js_subscribe_value)\n        ) {\n            return false;\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _canBtnPrimaryClosePopup(primaryBtnEl) {\n        if (primaryBtnEl.classList.contains('js_subscribe_btn')) {\n            return false;\n        }\n        return this._super(...arguments);\n    },\n});\n\nexport default PopupWidget;\n"], "file": "/web/assets/1/4079255/web.assets_frontend_lazy.js", "sourceRoot": "../../../../"}